diff --git a/.github/workflows/cli-build.yml b/.github/workflows/cli-build.yml new file mode 100644 index 0000000..3510e48 --- /dev/null +++ b/.github/workflows/cli-build.yml @@ -0,0 +1,34 @@ +name: Build CLI + +on: + push: + branches: + - main + paths: + - "cli/**" + - ".github/workflows/cli-build.yml" + pull_request: + paths: + - "cli/**" + - ".github/workflows/cli-build.yml" + workflow_dispatch: + +jobs: + build: + name: Build CLI (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build + working-directory: cli + run: cargo build --release diff --git a/.gitignore b/.gitignore index 8b74fb9..7b9e3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ public/assets/ # deps node_modules/ .wrangler +cli/target/ # env .env diff --git a/README.ja.md b/README.ja.md index d84a64a..9178fed 100644 --- a/README.ja.md +++ b/README.ja.md @@ -30,6 +30,51 @@ WebRTCを使ったP2Pファイル共有ツール。サーバーを経由せず - [Vite](https://vite.dev/) - SSR対応ビルドツール - WebRTC - P2Pデータ転送 +## CLI (Rust) + +`cli/` ディレクトリに Rust 製のCLIを用意しています。ブラウザ⇄ターミナル、ターミナル⇄ターミナルの転送に対応します。 + +### 対応プラットフォーム + +- **Linux** (x86_64) +- **macOS** (Intel / Apple Silicon) + +GitHub Actionsにより、`cli/` ディレクトリへのpush/PR時にビルドが自動テストされます。 + +```sh +cd cli +cargo run --release -- send /path/to/file +cargo run --release -- receive --output-dir ./downloads +``` + +暗号化は `send` のデフォルトです。`send` が出力する `#k=...` 付きのURLを、そのまま `receive` に渡せます。 + +```sh +cargo run --release -- send /path/to/file +cargo run --release -- receive "https://share-files.karakuri-maker.com/r/ROOM#k=..." +``` + +暗号化を無効にする場合は `--no-encrypt` を指定してください。 + +復号鍵を明示したい場合は `receive` に `--key`(base64url)を渡します。 + +```sh +cargo run --release -- receive --key --output-dir ./downloads +``` + +デフォルトでは、`send` と `receive` は転送成功後に終了します。継続して待ちたい場合は `--stay-open` を指定してください。 + +※ `#k=...` を含むURLはシェルでクォートしてください。従来の `--file` / `--room-id` も引き続き利用できます。 + +デフォルトではデモ環境へ接続します。`SHARE_FILES_ENDPOINT` 環境変数で上書きできます。 + +```sh +SHARE_FILES_ENDPOINT=https://share-files.karakuri-maker.com \ + cargo run --release -- send /path/to/file +``` + +既存ルームに参加したい場合は `--room-id` を明示指定してください。 + ## 必要環境 - [Bun](https://bun.sh/) ランタイム diff --git a/README.md b/README.md index 8f08f17..7a2d12d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,51 @@ A P2P file sharing tool using WebRTC. Transfer files directly between browsers w - [Vite](https://vite.dev/) - SSR-enabled build tool - WebRTC - P2P data transfer +## CLI (Rust) + +The `cli/` directory contains a Rust-based CLI that can send or receive files using the same WebRTC signaling flow, enabling browser ⇄ terminal and terminal ⇄ terminal transfers. + +### Supported Platforms + +- **Linux** (x86_64) +- **macOS** (Intel / Apple Silicon) + +Builds are automatically tested via GitHub Actions on push/PR to the `cli/` directory. + +```sh +cd cli +cargo run --release -- send /path/to/file +cargo run --release -- receive --output-dir ./downloads +``` + +Encrypted transfers are enabled by default for `send`. The command prints a room URL with `#k=...` that you can pass directly to `receive`: + +```sh +cargo run --release -- send /path/to/file +cargo run --release -- receive "https://share-files.karakuri-maker.com/r/ROOM#k=..." +``` + +To disable encryption for `send`, pass `--no-encrypt`. + +If you want to provide the decryption key explicitly, pass `--key` (base64url) to `receive`: + +```sh +cargo run --release -- receive --key --output-dir ./downloads +``` + +By default, `send` and `receive` exit after a successful transfer. Use `--stay-open` to keep the process running for additional transfers. + +Note: URLs with `#k=...` should be quoted in the shell. Legacy flags `--file` and `--room-id` are still accepted. + +By default it connects to the demo endpoint. Override it with the `SHARE_FILES_ENDPOINT` environment variable: + +```sh +SHARE_FILES_ENDPOINT=https://share-files.karakuri-maker.com \ + cargo run --release -- send /path/to/file +``` + +You can also provide `--room-id` explicitly if you want to join an existing room. + ## Prerequisites - [Bun](https://bun.sh/) runtime diff --git a/README.zh.md b/README.zh.md index bebf91e..7619262 100644 --- a/README.zh.md +++ b/README.zh.md @@ -30,6 +30,51 @@ - [Vite](https://vite.dev/) - 支持SSR的构建工具 - WebRTC - P2P数据传输 +## CLI (Rust) + +`cli/` 目录包含一个Rust编写的CLI工具,使用相同的WebRTC信令流程发送和接收文件,支持浏览器⇄终端和终端⇄终端的传输。 + +### 支持的平台 + +- **Linux** (x86_64) +- **macOS** (Intel / Apple Silicon) + +通过GitHub Actions,在向 `cli/` 目录push/PR时自动测试构建。 + +```sh +cd cli +cargo run --release -- send /path/to/file +cargo run --release -- receive --output-dir ./downloads +``` + +`send` 默认启用加密。`send` 输出的带有 `#k=...` 的URL可以直接传给 `receive`: + +```sh +cargo run --release -- send /path/to/file +cargo run --release -- receive "https://share-files.karakuri-maker.com/r/ROOM#k=..." +``` + +要禁用加密,请使用 `--no-encrypt`。 + +如需显式指定解密密钥,请向 `receive` 传递 `--key`(base64url编码): + +```sh +cargo run --release -- receive --key --output-dir ./downloads +``` + +默认情况下,`send` 和 `receive` 在传输成功后会退出。如需保持运行以进行更多传输,请使用 `--stay-open`。 + +※ 包含 `#k=...` 的URL在shell中需要用引号包裹。传统的 `--file` / `--room-id` 参数仍然可用。 + +默认连接到演示环境。可通过 `SHARE_FILES_ENDPOINT` 环境变量覆盖: + +```sh +SHARE_FILES_ENDPOINT=https://share-files.karakuri-maker.com \ + cargo run --release -- send /path/to/file +``` + +如需加入现有房间,请显式指定 `--room-id`。 + ## 环境要求 - [Bun](https://bun.sh/) 运行时 diff --git a/cli/Cargo.lock b/cli/Cargo.lock new file mode 100644 index 0000000..17e3f9c --- /dev/null +++ b/cli/Cargo.lock @@ -0,0 +1,3235 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.5", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "interceptor" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5927883184e6a819b22d5e4f5f7bc7ca134fde9b2026fbddd8d95249746ba21e" +dependencies = [ + "async-trait", + "bytes", + "log", + "rand 0.8.5", + "rtcp", + "rtp 0.9.0", + "thiserror 1.0.69", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.36", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6" +dependencies = [ + "pem", + "ring 0.16.20", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.5", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtcp" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33648a781874466a62d89e265fee9f17e32bc7d05a256e6cca41bf97eadcd8aa" +dependencies = [ + "bytes", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60482acbe8afb31edf6b1413103b7bca7a65004c423b3c3993749a083994fbe" +dependencies = [ + "bytes", + "rand 0.8.5", + "serde", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fca9bd66ae0b1f3f649b8f5003d6176433d7293b78b0fce7e1031816bdd99d" +dependencies = [ + "bytes", + "rand 0.8.5", + "serde", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "sdp" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13254db766b17451aced321e7397ebf0a446ef0c8d2942b6e67a95815421093f" +dependencies = [ + "rand 0.8.5", + "substring", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "share-files-cli" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.22.1", + "bytes", + "chrono", + "clap", + "futures-util", + "getrandom 0.2.16", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite", + "url", + "uuid", + "webrtc", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3f371788132e9d623e6eab4ba28aac083763a4133f045e6ebaee5ceb869803d" +dependencies = [ + "base64 0.21.7", + "crc", + "lazy_static", + "md-5", + "rand 0.8.5", + "ring 0.17.14", + "subtle", + "thiserror 1.0.69", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.23.36", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "turn" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb2ac4f331064513ad510b7a36edc0df555bd61672986607f7c9ff46f98f415" +dependencies = [ + "async-trait", + "base64 0.21.7", + "futures", + "log", + "md-5", + "rand 0.8.5", + "ring 0.17.14", + "stun", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "webrtc-util", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webrtc" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91e7cf018f7185552bf6a5dd839f4ed9827aea33b746763c9a215f84a0d0b34" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "cfg-if", + "hex", + "interceptor", + "lazy_static", + "log", + "rand 0.8.5", + "rcgen", + "regex", + "ring 0.16.20", + "rtcp", + "rtp 0.9.0", + "rustls 0.21.12", + "sdp", + "serde", + "serde_json", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.69", + "time", + "tokio", + "turn", + "url", + "waitgroup", + "webrtc-data", + "webrtc-dtls", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c08e648e10572b9edbe741074e0f4d3cb221aa7cdf9a814ee71606de312f33" +dependencies = [ + "bytes", + "log", + "thiserror 1.0.69", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-dtls" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b140b953f986e97828aa33ec6318186b05d862bee689efbc57af04a243e832" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "der-parser", + "hkdf", + "hmac", + "log", + "p256", + "p384", + "rand 0.8.5", + "rand_core 0.6.4", + "rcgen", + "ring 0.16.20", + "rustls 0.21.12", + "sec1", + "serde", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + +[[package]] +name = "webrtc-ice" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bbd6b3dea22cc6e961e22b012e843d8869e2ac8e76b96e54d4a25e311857ad" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce981f93104a8debb3563bb0cedfe4aa2f351fdf6b53f346ab50009424125c08" +dependencies = [ + "log", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280017b6b9625ef7329146332518b339c3cceff231cc6f6a9e0e6acab25ca4af" +dependencies = [ + "byteorder", + "bytes", + "rand 0.8.5", + "rtp 0.10.0", + "thiserror 1.0.69", +] + +[[package]] +name = "webrtc-sctp" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df75ec042002fe995194712cbeb2029107a60a7eab646f1b789eb1be94d0e367" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db1f36c1c81e4b1e531c0b9678ba0c93809e196ce62122d87259bb71c03b9f" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp 0.9.0", + "sha1", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e85154ef743d9a2a116d104faaaa82740a281b8b4bed5ee691a2df6c133d873" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "libc", + "log", + "nix", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring 0.16.20", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure 0.13.2", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..7b02f64 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "share-files-cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +aes-gcm = "0.10" +anyhow = "1.0" +base64 = "0.22" +bytes = "1.5" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +clap = { version = "4.5", features = ["derive"] } +futures-util = "0.3" +getrandom = "0.2" +mime_guess = "2.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +tokio = { version = "1.37", features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } +tokio-tungstenite = { version = "0.23", features = ["rustls-tls-webpki-roots"] } +url = "2.5" +uuid = { version = "1.8", features = ["v4"] } +webrtc = "0.9" diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..a87a5dd --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,1086 @@ +// Design: see README.md for the signaling flow; related to src/client/room.tsx. + +use aes_gcm::aead::{Aead, KeyInit}; +use aes_gcm::{Aes256Gcm, Nonce}; +use anyhow::{anyhow, Context, Result}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use bytes::Bytes; +use clap::{Parser, Subcommand}; +use futures_util::{SinkExt, StreamExt}; +use getrandom::getrandom; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::env; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::{mpsc, Mutex}; +use tokio::time::{sleep, Duration}; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::Message; +use url::form_urlencoded; +use url::Url; +use uuid::Uuid; +use webrtc::data_channel::data_channel_state::RTCDataChannelState; +use webrtc::api::interceptor_registry::register_default_interceptors; +use webrtc::api::media_engine::MediaEngine; +use webrtc::api::APIBuilder; +use webrtc::data_channel::data_channel_init::RTCDataChannelInit; +use webrtc::data_channel::data_channel_message::DataChannelMessage; +use webrtc::data_channel::RTCDataChannel; +use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit; +use webrtc::ice_transport::ice_server::RTCIceServer; +use webrtc::interceptor::registry::Registry; +use webrtc::peer_connection::configuration::RTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; +use webrtc::peer_connection::RTCPeerConnection; + +const AES_KEY_LEN: usize = 32; +const AES_NONCE_LEN: usize = 12; +const AES_TAG_LEN: usize = 16; +const MAX_FRAME_SIZE: usize = 16 * 1024; + +#[derive(Parser, Debug)] +#[command(name = "share-files-cli")] +#[command(about = "P2P file transfer CLI for share-files")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + Send { + #[arg(value_name = "FILE", help = "File to send")] + file: Option, + #[arg(long = "file", value_name = "PATH", help = "File to send (legacy --file)", hide = true)] + file_flag: Option, + #[arg(value_name = "ROOM_ID_OR_URL", help = "Room ID or full room URL (optional, supports #k=...)")] + room_input: Option, + #[arg(long = "room-id", value_name = "ROOM_ID_OR_URL", help = "Room ID or full room URL (legacy --room-id)", hide = true)] + room_id: Option, + #[arg(long, value_name = "URL", help = "Override signaling endpoint")] + endpoint: Option, + #[arg(long, help = "Disable E2E encryption (default: enabled)")] + no_encrypt: bool, + #[arg(long, help = "Keep running after a successful send")] + stay_open: bool, + }, + Receive { + #[arg(value_name = "ROOM_ID_OR_URL", help = "Room ID or full room URL (supports #k=...)")] + room_input: Option, + #[arg(long = "room-id", value_name = "ROOM_ID_OR_URL", help = "Room ID or full room URL (legacy --room-id)", hide = true)] + room_id: Option, + #[arg(long, default_value = ".", value_name = "DIR", help = "Output directory")] + output_dir: PathBuf, + #[arg(long, value_name = "URL", help = "Override signaling endpoint")] + endpoint: Option, + #[arg(long, value_name = "KEY", help = "Base64url decryption key (overrides #k=...)")] + key: Option, + #[arg(long, help = "Keep running after a successful receive")] + stay_open: bool, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +enum ServerMessage { + #[serde(rename = "role")] + Role { role: String, cid: String }, + #[serde(rename = "peers")] + Peers { count: u32 }, + #[serde(rename = "wait")] + Wait { position: Option }, + #[serde(rename = "start")] + Start { #[serde(rename = "peerId")] peer_id: Option }, + #[serde(rename = "peer-left")] + PeerLeft { #[serde(rename = "peerId")] peer_id: String }, + #[serde(rename = "offer")] + Offer { from: String, sid: u64, sdp: RTCSessionDescription }, + #[serde(rename = "answer")] + Answer { from: String, sid: u64, sdp: RTCSessionDescription }, + #[serde(rename = "candidate")] + Candidate { from: String, sid: u64, candidate: RTCIceCandidateInit }, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +enum ClientMessage { + #[serde(rename = "offer")] + Offer { to: String, sid: u64, sdp: RTCSessionDescription }, + #[serde(rename = "answer")] + Answer { to: String, sid: u64, sdp: RTCSessionDescription }, + #[serde(rename = "candidate")] + Candidate { to: String, sid: u64, candidate: RTCIceCandidateInit }, + #[serde(rename = "transfer-done")] + TransferDone { #[serde(rename = "peerId")] peer_id: String }, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +enum DataMessage { + #[serde(rename = "meta")] + Meta { + name: String, + size: u64, + mime: String, + encrypted: bool, + }, + #[serde(rename = "done")] + Done, +} + +struct RoomInput { + room_id: String, + endpoint: Option, + key: Option>, +} + +#[derive(Clone)] +struct FileInfo { + path: PathBuf, + name: String, + size: u64, + mime: String, +} + +struct OffererPeerState { + signal_sid: u64, + active_sid: Option, + pending_candidates: Vec, + remote_desc_set: bool, + sending: bool, +} + +struct PendingCandidate { + sid: u64, + candidate: RTCIceCandidateInit, +} + +struct OffererPeer { + peer_id: String, + pc: Arc, + state: Arc>, +} + +struct ReceiverState { + pc: Arc, + peer_id: Option, + active_sid: Option, + pending_candidates: Vec, + remote_desc_set: bool, +} + +struct ReceiveProgress { + output_dir: PathBuf, + current_file: Option, + file: Option, + expected_size: u64, + received: u64, + encrypted: bool, + crypto: Option>, + success_tx: Option>, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::Send { + file, + file_flag, + room_input, + room_id, + endpoint, + no_encrypt, + stay_open, + } => { + let file = file_flag + .or(file) + .ok_or_else(|| anyhow!("File path is required (usage: send )"))?; + let room_input = room_id.or(room_input); + run_send(room_input.as_deref(), &file, endpoint.as_deref(), no_encrypt, stay_open).await + } + Command::Receive { + room_input, + room_id, + output_dir, + endpoint, + key, + stay_open, + } => { + let room_input = room_id + .or(room_input) + .ok_or_else(|| anyhow!("Room ID or URL is required (usage: receive )"))?; + run_receive(&room_input, &output_dir, endpoint.as_deref(), key.as_deref(), stay_open).await + } + } +} + +async fn run_send( + room_id: Option<&str>, + file_path: &Path, + endpoint: Option<&str>, + no_encrypt: bool, + stay_open: bool, +) -> Result<()> { + let file_info = load_file_info(file_path).await?; + let mut endpoint_override = endpoint.map(|value| value.to_string()); + let mut room_key: Option> = None; + let client_id = Uuid::new_v4().to_string(); + let room_id = match room_id { + Some(value) => { + let parsed = parse_room_input(value)?; + if endpoint_override.is_none() { + endpoint_override = parsed.endpoint; + } + room_key = parsed.key; + parsed.room_id + } + None => create_room(endpoint_override.as_deref(), Some(&client_id)).await?, + }; + let encrypt = !no_encrypt; + let room_key = if encrypt { + Some(match room_key { + Some(key) => key, + None => generate_key()?.to_vec(), + }) + } else { + None + }; + let crypto = match room_key.as_deref() { + Some(key) => Some(Arc::new(build_crypto(key)?)), + None => None, + }; + let ws_url = build_ws_url(endpoint_override.as_deref(), &room_id, &client_id)?; + + log_line("[room] id", &room_id); + log_line( + "[room] url", + &build_room_url_with_key(endpoint_override.as_deref(), &room_id, room_key.as_deref())?, + ); + log_line("[ws] connecting", &ws_url.to_string()); + let (ws_stream, _) = connect_async(ws_url.to_string()) + .await + .context("connect signaling websocket")?; + let (mut ws_write, mut ws_read) = ws_stream.split(); + + let (signal_tx, mut signal_rx) = mpsc::unbounded_channel::(); + let (success_tx, mut success_rx) = mpsc::unbounded_channel::<()>(); + let success_tx = if stay_open { None } else { Some(success_tx) }; + + let writer = tokio::spawn(async move { + while let Some(msg) = signal_rx.recv().await { + let text = serde_json::to_string(&msg).map_err(|err| anyhow!(err))?; + ws_write.send(Message::Text(text)).await.map_err(|err| anyhow!(err))?; + } + Ok::<(), anyhow::Error>(()) + }); + + let peers: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + let file_info = Arc::new(file_info); + + let mut completed = false; + loop { + tokio::select! { + msg = ws_read.next() => { + let msg = match msg { + Some(msg) => msg.context("websocket read")?, + None => break, + }; + if let Message::Text(text) = msg { + let parsed: ServerMessage = match serde_json::from_str(&text) { + Ok(msg) => msg, + Err(_) => continue, + }; + + match parsed { + ServerMessage::Role { role, cid } => { + log_line("[ws] role", &format!("{role} ({cid})")); + if role != "offerer" { + return Err(anyhow!("This command must be the offerer; connect first or use receive.")); + } + } + ServerMessage::Peers { count } => { + log_line("[ws] peers", &count.to_string()); + } + ServerMessage::Wait { position } => { + let label = position.map(|p| p.to_string()).unwrap_or_else(|| "waiting".to_string()); + log_line("[ws] queue", &label); + } + ServerMessage::Start { peer_id } => { + if let Some(peer_id) = peer_id { + let peer = create_offerer_peer( + peer_id.clone(), + signal_tx.clone(), + file_info.clone(), + crypto.clone(), + success_tx.clone(), + ) + .await?; + peers.lock().await.insert(peer_id.clone(), peer); + } + } + ServerMessage::Answer { from, sid, sdp } => { + if let Some(peer) = peers.lock().await.get(&from).cloned() { + handle_answer(peer, sid, sdp).await?; + } + } + ServerMessage::Candidate { from, sid, candidate } => { + if let Some(peer) = peers.lock().await.get(&from).cloned() { + handle_offer_candidate(peer, sid, candidate).await?; + } + } + ServerMessage::PeerLeft { peer_id } => { + log_line("[ws] peer-left", &peer_id); + peers.lock().await.remove(&peer_id); + } + _ => {} + } + } + } + _ = success_rx.recv(), if !stay_open => { + log_line("[send] completed", "transfer done"); + let peers_snapshot = { + let guard = peers.lock().await; + guard.values().cloned().collect::>() + }; + for peer in peers_snapshot { + let _ = peer.pc.close().await; + } + completed = true; + break; + } + } + } + if completed { + writer.abort(); + return Ok(()); + } + drop(signal_tx); + writer.await??; + Ok(()) +} + +async fn run_receive( + room_input: &str, + output_dir: &Path, + endpoint: Option<&str>, + key: Option<&str>, + stay_open: bool, +) -> Result<()> { + let parsed = parse_room_input(room_input)?; + let mut key_override = parsed.key; + if let Some(key) = key { + key_override = Some(b64url_decode(key)?); + } + let endpoint_override = endpoint.or(parsed.endpoint.as_deref()); + let crypto = match key_override.as_deref() { + Some(key) => Some(Arc::new(build_crypto(key)?)), + None => None, + }; + let room_id = parsed.room_id; + let client_id = Uuid::new_v4().to_string(); + let ws_url = build_ws_url(endpoint_override, &room_id, &client_id)?; + + log_line("[room] id", &room_id); + log_line("[ws] connecting", &ws_url.to_string()); + let (ws_stream, _) = connect_async(ws_url.to_string()) + .await + .context("connect signaling websocket")?; + let (mut ws_write, mut ws_read) = ws_stream.split(); + + let (signal_tx, mut signal_rx) = mpsc::unbounded_channel::(); + let (success_tx, mut success_rx) = mpsc::unbounded_channel::<()>(); + let success_tx = if stay_open { None } else { Some(success_tx) }; + + let writer = tokio::spawn(async move { + while let Some(msg) = signal_rx.recv().await { + let text = serde_json::to_string(&msg).map_err(|err| anyhow!(err))?; + ws_write.send(Message::Text(text)).await.map_err(|err| anyhow!(err))?; + } + Ok::<(), anyhow::Error>(()) + }); + + let receiver_state: Arc>> = Arc::new(Mutex::new(None)); + let progress = Arc::new(Mutex::new(ReceiveProgress { + output_dir: output_dir.to_path_buf(), + current_file: None, + file: None, + expected_size: 0, + received: 0, + encrypted: false, + crypto, + success_tx, + })); + + let mut completed = false; + loop { + tokio::select! { + msg = ws_read.next() => { + let msg = match msg { + Some(msg) => msg.context("websocket read")?, + None => break, + }; + if let Message::Text(text) = msg { + let parsed: ServerMessage = match serde_json::from_str(&text) { + Ok(msg) => msg, + Err(_) => continue, + }; + + match parsed { + ServerMessage::Role { role, cid } => { + log_line("[ws] role", &format!("{role} ({cid})")); + if role != "answerer" { + return Err(anyhow!("This command must be the answerer; connect after the sender.")); + } + } + ServerMessage::Peers { count } => { + log_line("[ws] peers", &count.to_string()); + } + ServerMessage::Wait { position } => { + let label = position.map(|p| p.to_string()).unwrap_or_else(|| "waiting".to_string()); + log_line("[ws] queue", &label); + } + ServerMessage::Start { .. } => { + let pc = create_peer_connection().await?; + let tx = signal_tx.clone(); + let receiver_state_for_ice = receiver_state.clone(); + pc.on_ice_candidate(Box::new(move |candidate| { + let tx = tx.clone(); + let receiver_state = receiver_state_for_ice.clone(); + Box::pin(async move { + if let Some(candidate) = candidate { + let candidate = candidate.to_json().unwrap_or_default(); + let guard = receiver_state.lock().await; + if let Some(state) = guard.as_ref() { + if let (Some(peer_id), Some(sid)) = (state.peer_id.clone(), state.active_sid) { + let _ = tx.send(ClientMessage::Candidate { to: peer_id, sid, candidate }); + } + } + } + }) + })); + + let rx_progress = progress.clone(); + pc.on_data_channel(Box::new(move |dc| { + let rx_progress = rx_progress.clone(); + Box::pin(async move { + wire_receiver_channel(dc, rx_progress).await; + }) + })); + + *receiver_state.lock().await = Some(ReceiverState { + pc, + peer_id: None, + active_sid: None, + pending_candidates: Vec::new(), + remote_desc_set: false, + }); + } + ServerMessage::Offer { from, sid, sdp } => { + let mut guard = receiver_state.lock().await; + let state = guard.as_mut().ok_or_else(|| anyhow!("Receiver not initialized"))?; + state.peer_id = Some(from.clone()); + state.active_sid = Some(sid); + state.pc.set_remote_description(sdp).await?; + state.remote_desc_set = true; + flush_receiver_candidates(state).await?; + + let answer = state.pc.create_answer(None).await?; + state.pc.set_local_description(answer).await?; + if let Some(local) = state.pc.local_description().await { + let _ = signal_tx.send(ClientMessage::Answer { to: from, sid, sdp: local }); + } + } + ServerMessage::Candidate { from: _, sid, candidate } => { + let mut guard = receiver_state.lock().await; + if let Some(state) = guard.as_mut() { + handle_receiver_candidate(state, sid, candidate).await?; + } + } + _ => {} + } + } + } + _ = success_rx.recv(), if !stay_open => { + log_line("[recv] completed", "transfer done"); + if let Some(state) = receiver_state.lock().await.take() { + let _ = state.pc.close().await; + } + completed = true; + break; + } + } + } + if completed { + writer.abort(); + return Ok(()); + } + drop(signal_tx); + writer.await??; + Ok(()) +} + +async fn create_offerer_peer( + peer_id: String, + signal_tx: mpsc::UnboundedSender, + file_info: Arc, + crypto: Option>, + success_tx: Option>, +) -> Result> { + let pc = create_peer_connection().await?; + let dc = pc + .create_data_channel( + "file", + Some(RTCDataChannelInit { + ordered: Some(true), + ..Default::default() + }), + ) + .await?; + + let peer = Arc::new(OffererPeer { + peer_id: peer_id.clone(), + pc: pc.clone(), + state: Arc::new(Mutex::new(OffererPeerState { + signal_sid: 0, + active_sid: None, + pending_candidates: Vec::new(), + remote_desc_set: false, + sending: false, + })), + }); + + let peer_clone = peer.clone(); + let tx = signal_tx.clone(); + pc.on_ice_candidate(Box::new(move |candidate| { + let peer_clone = peer_clone.clone(); + let tx = tx.clone(); + Box::pin(async move { + if let Some(candidate) = candidate { + let candidate = candidate.to_json().unwrap_or_default(); + let sid = peer_clone.state.lock().await.active_sid; + if let Some(sid) = sid { + let _ = tx.send(ClientMessage::Candidate { + to: peer_clone.peer_id.clone(), + sid, + candidate, + }); + } + } + }) + })); + + pc.on_peer_connection_state_change(Box::new(move |state: RTCPeerConnectionState| { + Box::pin(async move { + log_line("[rtc] connectionState", &format!("{:?}", state)); + }) + })); + + let send_tx = signal_tx.clone(); + let send_peer_id = peer_id.clone(); + let file_info = file_info.clone(); + let send_state = peer.state.clone(); + let dc_for_open = dc.clone(); + let crypto = crypto.clone(); + let success_tx = success_tx.clone(); + dc.on_open(Box::new(move || { + let send_tx = send_tx.clone(); + let send_peer_id = send_peer_id.clone(); + let file_info = file_info.clone(); + let dc = dc_for_open.clone(); + let send_state = send_state.clone(); + let crypto = crypto.clone(); + let success_tx = success_tx.clone(); + Box::pin(async move { + let mut guard = send_state.lock().await; + if guard.sending { + return; + } + guard.sending = true; + drop(guard); + + if let Err(err) = send_file(&dc, &file_info, crypto).await { + log_line("[send] error", &format!("{err:#}")); + return; + } + let _ = send_tx.send(ClientMessage::TransferDone { peer_id: send_peer_id }); + if let Some(tx) = success_tx.as_ref() { + let _ = tx.send(()); + } + }) + })); + + send_offer(peer.clone(), signal_tx).await?; + + Ok(peer) +} + +async fn send_offer(peer: Arc, signal_tx: mpsc::UnboundedSender) -> Result<()> { + let mut guard = peer.state.lock().await; + if guard.active_sid.is_some() { + return Ok(()); + } + guard.signal_sid += 1; + let sid = guard.signal_sid; + guard.active_sid = Some(sid); + drop(guard); + + let offer = peer.pc.create_offer(None).await?; + peer.pc.set_local_description(offer).await?; + if let Some(local) = peer.pc.local_description().await { + let _ = signal_tx.send(ClientMessage::Offer { + to: peer.peer_id.clone(), + sid, + sdp: local, + }); + } + Ok(()) +} + +async fn handle_answer(peer: Arc, sid: u64, sdp: RTCSessionDescription) -> Result<()> { + let mut guard = peer.state.lock().await; + if guard.active_sid != Some(sid) { + return Ok(()); + } + peer.pc.set_remote_description(sdp).await?; + guard.remote_desc_set = true; + drop(guard); + flush_offer_candidates(peer).await?; + Ok(()) +} + +async fn handle_offer_candidate(peer: Arc, sid: u64, candidate: RTCIceCandidateInit) -> Result<()> { + let mut guard = peer.state.lock().await; + if guard.remote_desc_set { + drop(guard); + peer.pc.add_ice_candidate(candidate).await?; + } else { + guard.pending_candidates.push(PendingCandidate { sid, candidate }); + } + Ok(()) +} + +async fn flush_offer_candidates(peer: Arc) -> Result<()> { + let sid = peer.state.lock().await.active_sid; + if sid.is_none() { + return Ok(()); + } + let sid = sid.unwrap(); + let pending = { + let mut guard = peer.state.lock().await; + std::mem::take(&mut guard.pending_candidates) + }; + let mut remaining = Vec::new(); + for item in pending { + if item.sid == sid { + peer.pc.add_ice_candidate(item.candidate).await?; + } else { + remaining.push(item); + } + } + peer.state.lock().await.pending_candidates.extend(remaining); + Ok(()) +} + +async fn handle_receiver_candidate(state: &mut ReceiverState, sid: u64, candidate: RTCIceCandidateInit) -> Result<()> { + if state.remote_desc_set { + state.pc.add_ice_candidate(candidate).await?; + } else { + state.pending_candidates.push(PendingCandidate { sid, candidate }); + } + Ok(()) +} + +async fn flush_receiver_candidates(state: &mut ReceiverState) -> Result<()> { + let sid = state.active_sid; + if sid.is_none() { + return Ok(()); + } + let sid = sid.unwrap(); + let pending = std::mem::take(&mut state.pending_candidates); + let mut remaining = Vec::new(); + for item in pending { + if item.sid == sid { + state.pc.add_ice_candidate(item.candidate).await?; + } else { + remaining.push(item); + } + } + state.pending_candidates = remaining; + Ok(()) +} + +async fn wire_receiver_channel(dc: Arc, progress: Arc>) { + dc.on_message(Box::new(move |msg: DataChannelMessage| { + let progress = progress.clone(); + Box::pin(async move { + if msg.is_string { + if let Ok(text) = String::from_utf8(msg.data.to_vec()) { + if let Ok(parsed) = serde_json::from_str::(&text) { + match parsed { + DataMessage::Meta { name, size, mime, encrypted } => { + let mut guard = progress.lock().await; + if encrypted && guard.crypto.is_none() { + log_line("[recv] error", "encrypted files need a decryption key"); + return; + } + guard.encrypted = encrypted; + let safe_name = sanitize_file_name(&name); + let path = guard.output_dir.join(&safe_name); + match File::create(&path).await { + Ok(file) => { + guard.current_file = Some(path); + guard.file = Some(file); + guard.expected_size = size; + guard.received = 0; + log_line("[recv] meta", &format!("{safe_name} ({mime}, {size} bytes)")); + } + Err(err) => { + log_line("[recv] error", &format!("{err:#}")); + } + } + } + DataMessage::Done => { + let mut guard = progress.lock().await; + guard.file = None; + guard.encrypted = false; + if let Some(tx) = guard.success_tx.take() { + let _ = tx.send(()); + } + if let Some(path) = guard.current_file.take() { + log_line("[recv] completed", &path.display().to_string()); + } + } + } + } + } + return; + } + + let (encrypted, crypto) = { + let guard = progress.lock().await; + (guard.encrypted, guard.crypto.clone()) + }; + + let payload = if encrypted { + let crypto = match crypto.as_ref() { + Some(crypto) => crypto, + None => { + log_line("[recv] error", "encrypted chunk received without key"); + return; + } + }; + match decrypt_frame(crypto, msg.data.as_ref()) { + Ok(plain) => plain, + Err(err) => { + log_line("[recv] error", &format!("{err:#}")); + return; + } + } + } else { + msg.data.to_vec() + }; + + let mut guard = progress.lock().await; + if let Some(file) = guard.file.as_mut() { + if file.write_all(&payload).await.is_ok() { + guard.received += payload.len() as u64; + if guard.expected_size > 0 && guard.received >= guard.expected_size { + guard.file = None; + guard.encrypted = false; + if let Some(tx) = guard.success_tx.take() { + let _ = tx.send(()); + } + if let Some(path) = guard.current_file.take() { + log_line("[recv] completed", &path.display().to_string()); + } + } + } + } + }) + })); +} + +async fn send_file(dc: &RTCDataChannel, info: &FileInfo, crypto: Option>) -> Result<()> { + let encrypted = crypto.is_some(); + let meta = serde_json::json!({ + "type": "meta", + "name": info.name, + "size": info.size, + "mime": info.mime, + "encrypted": encrypted, + }); + let meta_text = serde_json::to_string(&meta)?; + dc.send_text(meta_text).await?; + + let chunk_size = if encrypted { + MAX_FRAME_SIZE - AES_NONCE_LEN - AES_TAG_LEN + } else { + MAX_FRAME_SIZE + }; + let mut file = File::open(&info.path).await?; + let mut buffer = vec![0u8; chunk_size]; + loop { + let read = file.read(&mut buffer).await?; + if read == 0 { + break; + } + let payload = if let Some(crypto) = crypto.as_ref() { + Bytes::from(encrypt_frame(crypto, &buffer[..read])?) + } else { + Bytes::copy_from_slice(&buffer[..read]) + }; + dc.send(&payload).await?; + } + + dc.send_text("{\"type\":\"done\"}").await?; + wait_for_drain(dc).await; + Ok(()) +} + +async fn load_file_info(path: &Path) -> Result { + let metadata = tokio::fs::metadata(path).await?; + let size = metadata.len(); + let name = path + .file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| anyhow!("Invalid file name"))? + .to_string(); + let mime = mime_guess::from_path(path) + .first_or_octet_stream() + .essence_str() + .to_string(); + Ok(FileInfo { + path: path.to_path_buf(), + name, + size, + mime, + }) +} + +async fn create_peer_connection() -> Result> { + let mut media_engine = MediaEngine::default(); + media_engine.register_default_codecs()?; + + let mut registry = Registry::new(); + registry = register_default_interceptors(registry, &mut media_engine)?; + + let api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build(); + + let config = RTCConfiguration { + ice_servers: vec![RTCIceServer { + urls: vec!["stun:stun.cloudflare.com:3478".to_string()], + ..Default::default() + }], + ..Default::default() + }; + + let pc = api.new_peer_connection(config).await?; + Ok(Arc::new(pc)) +} + +fn parse_room_input(value: &str) -> Result { + if let Ok(url) = Url::parse(value) { + return parse_room_url(&url); + } + let mut room_id = value; + let mut key = None; + if let Some((id, fragment)) = value.split_once('#') { + room_id = id; + key = parse_key_fragment(fragment)?; + } + let room_id = room_id.trim(); + if room_id.is_empty() { + return Err(anyhow!("Room ID is required")); + } + Ok(RoomInput { + room_id: room_id.to_string(), + endpoint: None, + key, + }) +} + +fn parse_room_url(url: &Url) -> Result { + let room_id = extract_room_id_from_url(url)?; + let endpoint = Some(base_endpoint_url(Some(url.as_str()))?.to_string()); + let key = match url.fragment() { + Some(fragment) => parse_key_fragment(fragment)?, + None => None, + }; + Ok(RoomInput { room_id, endpoint, key }) +} + +fn extract_room_id_from_url(url: &Url) -> Result { + let segments: Vec<_> = url + .path_segments() + .map(|segments| segments.filter(|segment| !segment.is_empty()).collect()) + .unwrap_or_default(); + match segments.as_slice() { + ["r", room_id, ..] => Ok(room_id.to_string()), + [room_id] => Ok(room_id.to_string()), + _ => Err(anyhow!("Room ID not found in URL path")), + } +} + +fn parse_key_fragment(fragment: &str) -> Result>> { + for (key, value) in form_urlencoded::parse(fragment.as_bytes()) { + if key == "k" { + return Ok(Some(b64url_decode(&value)?)); + } + } + Ok(None) +} + +fn build_crypto(key: &[u8]) -> Result { + Aes256Gcm::new_from_slice(key).map_err(|_| anyhow!("Invalid encryption key length")) +} + +fn generate_key() -> Result<[u8; AES_KEY_LEN]> { + let mut key = [0u8; AES_KEY_LEN]; + getrandom(&mut key).map_err(|err| anyhow!(err))?; + Ok(key) +} + +fn encrypt_frame(crypto: &Aes256Gcm, plain: &[u8]) -> Result> { + let mut nonce_bytes = [0u8; AES_NONCE_LEN]; + getrandom(&mut nonce_bytes).map_err(|err| anyhow!(err))?; + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = crypto.encrypt(nonce, plain).map_err(|err| anyhow!(err))?; + let mut frame = Vec::with_capacity(AES_NONCE_LEN + ciphertext.len()); + frame.extend_from_slice(&nonce_bytes); + frame.extend_from_slice(&ciphertext); + Ok(frame) +} + +fn decrypt_frame(crypto: &Aes256Gcm, frame: &[u8]) -> Result> { + if frame.len() < AES_NONCE_LEN { + return Err(anyhow!("Encrypted frame is too short")); + } + let (nonce_bytes, ciphertext) = frame.split_at(AES_NONCE_LEN); + let nonce = Nonce::from_slice(nonce_bytes); + crypto.decrypt(nonce, ciphertext).map_err(|err| anyhow!(err)) +} + +fn b64url_encode(value: &[u8]) -> String { + URL_SAFE_NO_PAD.encode(value) +} + +fn b64url_decode(value: &str) -> Result> { + URL_SAFE_NO_PAD.decode(value).map_err(|err| anyhow!(err)) +} + +fn sanitize_file_name(name: &str) -> String { + let candidate = Path::new(name) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("file"); + let trimmed = candidate.trim(); + if trimmed.is_empty() || trimmed == "." || trimmed == ".." { + "file".to_string() + } else { + trimmed.to_string() + } +} + +async fn wait_for_drain(dc: &RTCDataChannel) { + for _ in 0..500 { + if dc.ready_state() != RTCDataChannelState::Open { + break; + } + if dc.buffered_amount().await == 0 { + break; + } + sleep(Duration::from_millis(10)).await; + } +} + +fn build_ws_url(endpoint: Option<&str>, room_id: &str, client_id: &str) -> Result { + let mut url = base_endpoint_url(endpoint)?; + let scheme = match url.scheme() { + "https" => "wss", + "http" => "ws", + "wss" => "wss", + "ws" => "ws", + other => return Err(anyhow!("Unsupported endpoint scheme: {other}")), + }; + url.set_scheme(scheme).map_err(|_| anyhow!("Invalid endpoint scheme"))?; + url.set_path(&format!("/ws/{room_id}")); + url.set_query(Some(&format!("cid={client_id}"))); + Ok(url) +} + +fn build_room_url_with_key(endpoint: Option<&str>, room_id: &str, key: Option<&[u8]>) -> Result { + let mut url = base_endpoint_url(endpoint)?; + url.set_path(&format!("/r/{room_id}")); + url.set_query(None); + if let Some(key) = key { + url.set_fragment(Some(&format!("k={}", b64url_encode(key)))); + } else { + url.set_fragment(None); + } + Ok(url.to_string()) +} + +fn base_endpoint_url(endpoint: Option<&str>) -> Result { + let default_endpoint = "https://share-files.karakuri-maker.com"; + let env_endpoint = env::var("SHARE_FILES_ENDPOINT").ok(); + let endpoint = endpoint + .map(|value| value.to_string()) + .or(env_endpoint) + .unwrap_or_else(|| default_endpoint.to_string()); + + let mut url = Url::parse(&endpoint)?; + let scheme = match url.scheme() { + "https" | "http" => url.scheme().to_string(), + "wss" => "https".to_string(), + "ws" => "http".to_string(), + other => return Err(anyhow!("Unsupported endpoint scheme: {other}")), + }; + url.set_scheme(&scheme).map_err(|_| anyhow!("Invalid endpoint scheme"))?; + url.set_path(""); + url.set_query(None); + url.set_fragment(None); + Ok(url) +} + +async fn create_room(endpoint: Option<&str>, creator_cid: Option<&str>) -> Result { + #[derive(Serialize)] + struct RoomRequest { + #[serde(rename = "creatorCid", skip_serializing_if = "Option::is_none")] + creator_cid: Option, + } + + #[derive(Deserialize)] + struct RoomResponse { + #[serde(rename = "roomId")] + room_id: String, + } + + let mut url = base_endpoint_url(endpoint)?; + url.set_path("/api/rooms"); + let client = reqwest::Client::new(); + let response = client + .post(url) + .json(&RoomRequest { + creator_cid: creator_cid.map(|value| value.to_string()), + }) + .send() + .await + .context("create room request")?; + let response = response.error_for_status().context("create room response")?; + let body: RoomResponse = response.json().await.context("parse room response")?; + Ok(body.room_id) +} + +fn log_line(label: &str, value: &str) { + let now = chrono::Utc::now().format("%H:%M:%S%.3f"); + println!("[{now}] {label}: {value}"); +} diff --git a/public/style.css b/public/style.css index a8e233f..75daeff 100644 --- a/public/style.css +++ b/public/style.css @@ -453,6 +453,124 @@ input[type="checkbox"] { padding: 32px; } +/* Step Guide */ +.stepGuide { + margin-bottom: 32px; + padding: 24px; + border: var(--border); + background: var(--black); + color: var(--white); + position: relative; +} + +.stepGuide::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 8px, + rgba(255, 255, 255, 0.03) 8px, + rgba(255, 255, 255, 0.03) 16px + ); + pointer-events: none; +} + +.stepLabel { + font-family: "Bebas Neue", sans-serif; + font-size: 14px; + letter-spacing: 3px; + color: var(--gray); + margin-bottom: 8px; +} + +.stepMain { + font-family: "Bebas Neue", sans-serif; + font-size: 32px; + letter-spacing: 2px; + line-height: 1.2; + text-transform: uppercase; +} + +.stepSub { + font-size: 12px; + color: var(--gray); + margin-top: 8px; + letter-spacing: 1px; +} + +.stepGuide.waiting .stepMain::after { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + background: var(--red); + margin-left: 12px; + animation: pulse 1.5s ease-in-out infinite; + vertical-align: middle; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} + +.stepGuide.complete { + background: var(--white); + color: var(--black); + border-color: var(--black); +} + +.stepGuide.complete::before { + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 8px, + rgba(0, 0, 0, 0.03) 8px, + rgba(0, 0, 0, 0.03) 16px + ); +} + +.stepGuide.complete .stepLabel { + color: var(--gray); +} + +.stepGuide.complete .stepSub { + color: var(--gray); +} + +.stepProgress { + display: flex; + gap: 4px; + margin-top: 16px; +} + +.stepDot { + width: 24px; + height: 4px; + background: rgba(255, 255, 255, 0.2); +} + +.stepDot.done { + background: var(--white); +} + +.stepDot.current { + background: var(--red); +} + +.stepGuide.complete .stepDot { + background: rgba(0, 0, 0, 0.1); +} + +.stepGuide.complete .stepDot.done { + background: var(--black); +} + .pane { display: grid; gap: 24px; @@ -586,6 +704,32 @@ input[type="checkbox"] { } } +/* Toast notification */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(100px); + padding: 12px 24px; + background: var(--black); + color: var(--white); + font-size: 12px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + border: 4px solid var(--black); + box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3); + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + z-index: 1000; + pointer-events: none; +} + +.toast.show { + transform: translateX(-50%) translateY(0); + opacity: 1; +} + @media (max-width: 600px) { .topbar { flex-direction: column; diff --git a/src/client/room.tsx b/src/client/room.tsx index 4021834..e0ae5eb 100644 --- a/src/client/room.tsx +++ b/src/client/room.tsx @@ -74,15 +74,18 @@ const t = getT(); const root = document.querySelector("main.container"); const roomId = document.body.dataset.roomId; +const roomMaxConcurrent = Number.parseInt(document.body.dataset.maxConcurrent || "", 10); +const maxConcurrent = Number.isFinite(roomMaxConcurrent) ? roomMaxConcurrent : 3; if (root && roomId && document.getElementById("roomView")) { - render(, root); + render(, root); } type RoomAppProps = { roomId: string; + maxConcurrent: number; }; -function RoomApp({ roomId }: RoomAppProps) { +function RoomApp({ roomId, maxConcurrent }: RoomAppProps) { const [status, setStatus] = useState(t.status.initializing); const [role, setRole] = useState(null); const [peers, setPeers] = useState(0); @@ -91,6 +94,7 @@ function RoomApp({ roomId }: RoomAppProps) { const [recvProgress, setRecvProgress] = useState({ got: 0, total: 0 }); const [download, setDownload] = useState(null); const [dropHover, setDropHover] = useState(false); + const [toastVisible, setToastVisible] = useState(false); const roleRef = useRef(role); const peersRef = useRef(peers); @@ -132,13 +136,27 @@ function RoomApp({ roomId }: RoomAppProps) { } }, []); + const toastTimeoutRef = useRef | null>(null); + + const showToast = useCallback(() => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + setToastVisible(true); + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + }, 2000); + }, []); + const handleCopyLink = useCallback(() => { copyText(location.href); - }, []); + showToast(); + }, [showToast]); const handleCopyCode = useCallback(() => { copyText(roomId); - }, [roomId]); + showToast(); + }, [roomId, showToast]); const handleDragOver = useCallback((ev: DragEvent) => { ev.preventDefault(); @@ -511,6 +529,9 @@ function RoomApp({ roomId }: RoomAppProps) { if (msg.type === "peers") { peersRef.current = msg.count; setPeers(msg.count); + if (roleRef.current === "offerer" && msg.count > 0) { + setStatus(t.status.waitingSenderReady); + } return; } @@ -523,6 +544,9 @@ function RoomApp({ roomId }: RoomAppProps) { if (msg.type === "start") { if (roleRef.current === "offerer" && msg.peerId) { + if (offererPeersRef.current.has(msg.peerId)) { + closeOffererPeer(msg.peerId); + } const peer = createOffererPeer(msg.peerId); await sendOffer(peer); return; @@ -631,7 +655,7 @@ function RoomApp({ roomId }: RoomAppProps) { return t.role.unknown; }, [role]); - const peersLabel = useMemo(() => String(peers), [peers]); + const peersLabel = useMemo(() => `${peers}${t.room.peersUnit}`, [peers]); const sendPct = useMemo( () => progressPercent(sendProgress.sent, sendProgress.total), @@ -656,10 +680,93 @@ function RoomApp({ roomId }: RoomAppProps) { return `${selectedFile.name} (${formatBytes(selectedFile.size)})`; }, [selectedFile]); + const maxConcurrentLabel = useMemo( + () => t.room.maxConcurrentLimit.replace("{max}", String(maxConcurrent)), + [maxConcurrent] + ); + const canSend = role === "offerer" && !!selectedFile; const showSender = role === "offerer"; const showReceiver = role === "answerer"; + const sendHint = useMemo(() => { + if (!selectedFile) return t.room.sendHintNoFile; + if (peers === 0) return t.room.sendHintNoPeer; + return t.room.sendHintReady; + }, [selectedFile, peers]); + + const showSendProgress = sendProgress.total > 0; + + // Step guide logic + // Note: peers count includes self, so peers > 1 means someone else is connected + const maxStepRef = useRef(1); + + const guideState = useMemo(() => { + type GuideState = { step: number; main: string; sub: string; waiting: boolean; complete: boolean }; + + const receiverSteps: Record = { + 1: { step: 1, main: t.guide.receiverConnecting, sub: t.guide.receiverConnectingSub, waiting: true, complete: false }, + 2: { step: 2, main: t.guide.receiverWaitFile, sub: t.guide.receiverWaitFileSub, waiting: true, complete: false }, + 3: { step: 3, main: t.guide.receiverReceiving, sub: t.guide.receiverReceivingSub, waiting: false, complete: false }, + 4: { step: 4, main: t.guide.receiverComplete, sub: t.guide.receiverCompleteSub, waiting: false, complete: true }, + }; + + if (role === "offerer") { + // Sender: determine current step based on state + let currentStep = 1; + let state: GuideState; + + if (sendProgress.sent > 0 && sendProgress.sent >= sendProgress.total && sendProgress.total > 0) { + currentStep = 4; + state = { step: 4, main: t.guide.senderComplete, sub: t.guide.senderCompleteSub, waiting: false, complete: true }; + } else if (sendProgress.total > 0) { + currentStep = 3; + state = { step: 3, main: t.guide.senderSending, sub: t.guide.senderSendingSub, waiting: false, complete: false }; + } else if (peers > 1 && selectedFile) { + // File selected and peer connected - ready to send + currentStep = 3; + state = { step: 3, main: t.guide.senderReadyToSend, sub: t.guide.senderReadyToSendSub, waiting: false, complete: false }; + } else if (peers > 1) { + // Peer connected but no file yet + currentStep = 2; + state = { step: 2, main: t.guide.senderSelectFile, sub: t.guide.senderSelectFileSub, waiting: false, complete: false }; + } else { + // Waiting for peer + currentStep = 1; + state = { step: 1, main: t.guide.senderShareLink, sub: t.guide.senderShareLinkSub, waiting: true, complete: false }; + } + + // Never go back in step number + if (currentStep > maxStepRef.current) { + maxStepRef.current = currentStep; + } + // But always show current state's content (for file selection feedback) + return { ...state, step: Math.max(state.step, maxStepRef.current) }; + } + + if (role === "answerer") { + // Receiver steps: 1=connecting, 2=wait file, 3=receiving, 4=complete + let currentStep = 2; + if (download) { + currentStep = 4; + } else if (recvProgress.total > 0) { + currentStep = 3; + } + // Never go back + if (currentStep > maxStepRef.current) { + maxStepRef.current = currentStep; + } + return receiverSteps[maxStepRef.current]; + } + + // Initial connecting state (before role is assigned) + return receiverSteps[1]; + }, [role, peers, sendProgress, recvProgress, download, selectedFile]); + + const stepLabel = t.guide.stepLabel + .replace("{current}", String(guideState.step)) + .replace("{total}", "4"); + return (
@@ -671,8 +778,8 @@ function RoomApp({ roomId }: RoomAppProps) {
{status}
- - + +
@@ -685,9 +792,24 @@ function RoomApp({ roomId }: RoomAppProps) {
{peersLabel}
{t.room.encryptHint}
+
{maxConcurrentLabel}
+
+
{stepLabel}
+
{guideState.main}
+
{guideState.sub}
+
+ {[1, 2, 3, 4].map((n) => ( +
+ ))} +
+
+
- -
{selectedFileLabel}
+
{selectedFileLabel || sendHint}
-
-
-
+ {showSendProgress && ( +
+
+
+
+
{sendText}
-
{sendText}
-
+ )}
-
{t.room.waiting}
- -
-
-
+ {!recvProgress.total && !download && ( +
{t.room.waiting}
+ )} + + {recvProgress.total > 0 && ( +
+
+
+
+
{recvText}
-
{recvText}
-
+ )}
+
{t.room.copied}
); } diff --git a/src/index.tsx b/src/index.tsx index 1dd32f8..c5b798e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,12 +6,32 @@ import { detectLocale, getTranslations, supportedLocales, type Locale } from "./ type Bindings = CloudflareBindings & { ROOM: DurableObjectNamespace; + ROOM_RATE_LIMITER: RateLimit; }; const app = new Hono<{ Bindings: Bindings }>(); app.use("*", jsxRenderer()); +const DEFAULT_MAX_CONCURRENT = 3; +const MAX_MAX_CONCURRENT = 10; + +function normalizeMaxConcurrent(value?: number) { + const base = Number.isFinite(value) ? Math.floor(value!) : DEFAULT_MAX_CONCURRENT; + return Math.min(MAX_MAX_CONCURRENT, Math.max(1, base)); +} + +app.use("/api/rooms", async (c, next) => { + if (c.req.method !== "POST") return next(); + const ip = c.req.header("cf-connecting-ip") ?? "unknown"; + const key = `create-room:${ip}`; + const outcome = await c.env.ROOM_RATE_LIMITER.limit({ key }); + if (!outcome.success) { + return c.text("Rate limit exceeded", 429); + } + return next(); +}); + function getLocaleFromRequest(c: { req: { header: (name: string) => string | undefined; query: (name: string) => string | undefined } }): Locale { // Check query parameter first (for language switcher) const queryLang = c.req.query("lang"); @@ -28,15 +48,20 @@ app.get("/", (c) => { return c.render(); }); -app.get("/r/:roomId", (c) => { +app.get("/r/:roomId", async (c) => { const locale = getLocaleFromRequest(c); const t = getTranslations(locale); - return c.render(); + const roomId = c.req.param("roomId"); + const id = c.env.ROOM.idFromName(roomId); + const stub = c.env.ROOM.get(id); + const config = (await stub.fetch("https://room/config").then((res) => res.json())) as { maxConcurrent?: number }; + const maxConcurrent = normalizeMaxConcurrent(config.maxConcurrent); + return c.render(); }); app.post("/api/rooms", async (c) => { const body = (await c.req.json()) as { maxConcurrent?: number; creatorCid?: string }; - const maxConcurrent = Number.isFinite(body.maxConcurrent) ? Math.max(1, Math.floor(body.maxConcurrent!)) : 3; + const maxConcurrent = normalizeMaxConcurrent(body.maxConcurrent); const creatorCid = typeof body.creatorCid === "string" ? body.creatorCid : undefined; const roomId = generateRoomId(10); const id = c.env.ROOM.idFromName(roomId); diff --git a/src/locales/en.json b/src/locales/en.json index e84e233..917465f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -15,7 +15,7 @@ "step3": "Send", "sendTitle": "Start sending", "encryptOn": "E2E encryption ON", - "maxConcurrent": "Max concurrent", + "maxConcurrent": "Max concurrent (1-10)", "createRoom": "Create room", "receiveTitle": "Join to receive", "codePlaceholder": "Enter code (e.g. ABCD...)", @@ -26,44 +26,76 @@ "room": { "eyebrow": "ROOM—SESSION", "roomLabel": "ROOM", - "copyLink": "LINK", - "copyCode": "CODE", - "roleLabel": "ROLE", - "peersLabel": "PEERS", - "encryptHint": "With encryption ON, the decryption key is in the \"#\" part of the link (not sent to server).", - "dropTitle": "DROP FILE HERE", + "copyLink": "Copy Link", + "copyLinkHint": "Copy full URL (share with recipient)", + "copyCode": "Copy Code", + "copyCodeHint": "Copy room ID only", + "copied": "Copied!", + "roleLabel": "Your role", + "peersLabel": "Connected", + "peersUnit": "", + "maxConcurrentLimit": "Max concurrent: {max}", + "encryptHint": "Only people with the link can receive files", + "dropTitle": "Drop file here", "dropOr": "or", - "select": "SELECT", - "send": "SEND", + "select": "Select file", + "send": "Send", + "sendHintNoFile": "Please select a file", + "sendHintNoPeer": "Waiting for recipient to connect", + "sendHintReady": "Ready to send", "waiting": "Waiting for sender to select a file...", - "download": "DOWNLOAD" + "download": "Download" + }, + "guide": { + "stepLabel": "STEP {current} / {total}", + "senderShareLink": "Share the link with the recipient", + "senderShareLinkSub": "Copy the link or code and send it to them", + "senderWaitPeer": "Waiting for recipient to join...", + "senderWaitPeerSub": "The transfer will start once they connect", + "senderSelectFile": "Select a file to send", + "senderSelectFileSub": "Drag & drop or click to choose", + "senderReadyToSend": "Press Send to start", + "senderReadyToSendSub": "Ready to transfer!", + "senderSending": "Sending file...", + "senderSendingSub": "Transfer in progress", + "senderComplete": "Transfer complete!", + "senderCompleteSub": "File sent successfully", + "receiverConnecting": "Connecting to room...", + "receiverConnectingSub": "Please wait", + "receiverWaitFile": "Waiting for sender...", + "receiverWaitFileSub": "The sender will select a file", + "receiverReceiving": "Receiving file...", + "receiverReceivingSub": "Download will start automatically", + "receiverComplete": "Ready to download!", + "receiverCompleteSub": "Click the button below" }, "status": { "initializing": "Initializing...", - "connecting": "Connecting to signaling...", - "waitingSender": "Waiting as sender...", - "waitingReceiver": "Waiting as receiver...", - "dataChannelReady": "Data channel ready", - "dataChannelClosed": "Data channel closed", - "dataChannelError": "Data channel error", - "generatingFile": "Generating file...", - "receiveComplete": "Receive complete", + "connecting": "Connecting...", + "waitingSender": "Waiting for recipient to connect...", + "waitingSenderReady": "Select a file and send", + "waitingReceiver": "Waiting for sender...", + "dataChannelReady": "Connected!", + "dataChannelClosed": "Connection closed", + "dataChannelError": "Connection error", + "generatingFile": "Preparing file...", + "receiveComplete": "Receive complete!", "sending": "Sending...", - "sendComplete": "Send complete", - "queued": "Queued...", + "sendComplete": "Send complete!", + "queued": "In queue...", "preparing": "Preparing connection...", - "missingKey": "Encryption key not found (#k=... required)", + "missingKey": "Incomplete link (URL fragment after # is required)", "receiving": "Receiving: {name}", - "disconnected": "Signaling disconnected" + "disconnected": "Disconnected" }, "role": { - "offerer": "Sender (offerer)", - "answerer": "Receiver (answerer)", + "offerer": "Sender", + "answerer": "Receiver", "unknown": "—" }, "error": { "roomCreationFailed": "Failed to create room", - "wsConnectionFailed": "WebSocket connection failed", + "wsConnectionFailed": "Connection failed", "generic": "Error: {message}" } } diff --git a/src/locales/ja.json b/src/locales/ja.json index 6526192..602c747 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -15,7 +15,7 @@ "step3": "送信", "sendTitle": "送信を開始", "encryptOn": "E2E暗号化ON", - "maxConcurrent": "同時送信上限", + "maxConcurrent": "同時送信上限 (1-10)", "createRoom": "ルーム作成", "receiveTitle": "受信に参加", "codePlaceholder": "コード入力(例: ABCD...)", @@ -26,44 +26,76 @@ "room": { "eyebrow": "ROOM—SESSION", "roomLabel": "ROOM", - "copyLink": "LINK", - "copyCode": "CODE", - "roleLabel": "ROLE", - "peersLabel": "PEERS", - "encryptHint": "暗号化ONの場合、リンクの「#」以降に復号鍵が含まれます(サーバには送られません)。", - "dropTitle": "DROP FILE HERE", + "copyLink": "リンクをコピー", + "copyLinkHint": "URL全体をコピー(相手に送ってください)", + "copyCode": "コードをコピー", + "copyCodeHint": "ルームIDのみコピー", + "copied": "コピーしました!", + "roleLabel": "あなたの役割", + "peersLabel": "接続中", + "peersUnit": "人", + "maxConcurrentLimit": "同時送信上限: {max}人", + "encryptHint": "リンクを知っている人だけが受信できます", + "dropTitle": "ここにファイルをドロップ", "dropOr": "または", - "select": "SELECT", - "send": "SEND", + "select": "ファイルを選択", + "send": "送信", + "sendHintNoFile": "ファイルを選択してください", + "sendHintNoPeer": "相手の接続を待っています", + "sendHintReady": "送信できます", "waiting": "送信側がファイルを選ぶのを待っています...", - "download": "DOWNLOAD" + "download": "ダウンロード" + }, + "guide": { + "stepLabel": "STEP {current} / {total}", + "senderShareLink": "リンクを相手に送ってください", + "senderShareLinkSub": "リンクまたはコードをコピーして共有", + "senderWaitPeer": "相手の参加を待っています...", + "senderWaitPeerSub": "接続されると転送を開始できます", + "senderSelectFile": "送信するファイルを選択", + "senderSelectFileSub": "ドラッグ&ドロップまたはクリック", + "senderReadyToSend": "送信ボタンを押してください", + "senderReadyToSendSub": "準備完了!", + "senderSending": "送信中...", + "senderSendingSub": "ファイルを転送しています", + "senderComplete": "送信完了!", + "senderCompleteSub": "ファイルを送信しました", + "receiverConnecting": "ルームに接続中...", + "receiverConnectingSub": "しばらくお待ちください", + "receiverWaitFile": "送信者を待っています...", + "receiverWaitFileSub": "送信者がファイルを選択します", + "receiverReceiving": "受信中...", + "receiverReceivingSub": "自動でダウンロードが始まります", + "receiverComplete": "ダウンロード準備完了!", + "receiverCompleteSub": "下のボタンをクリック" }, "status": { "initializing": "初期化中...", - "connecting": "シグナリング接続中...", - "waitingSender": "送信側として待機中...", - "waitingReceiver": "受信側として待機中...", - "dataChannelReady": "データチャネル準備完了", - "dataChannelClosed": "データチャネル切断", - "dataChannelError": "データチャネルエラー", - "generatingFile": "ファイル生成中...", - "receiveComplete": "受信完了", + "connecting": "接続中...", + "waitingSender": "相手の接続を待っています...", + "waitingSenderReady": "ファイルを選んで送信してください", + "waitingReceiver": "送信側の準備を待っています...", + "dataChannelReady": "接続完了!", + "dataChannelClosed": "接続が切れました", + "dataChannelError": "接続エラー", + "generatingFile": "ファイルを準備中...", + "receiveComplete": "受信完了!", "sending": "送信中...", - "sendComplete": "送信完了", - "queued": "順番待ち...", + "sendComplete": "送信完了!", + "queued": "順番待ち中...", "preparing": "接続準備中...", - "missingKey": "暗号化リンクの鍵が見つかりません(#k=... が必要)", + "missingKey": "暗号化リンクが不完全です(URLの#以降が必要)", "receiving": "受信中: {name}", - "disconnected": "シグナリング切断" + "disconnected": "切断されました" }, "role": { - "offerer": "送信側(offerer)", - "answerer": "受信側(answerer)", + "offerer": "送信側", + "answerer": "受信側", "unknown": "—" }, "error": { "roomCreationFailed": "ルーム作成に失敗しました", - "wsConnectionFailed": "WebSocket接続に失敗しました", + "wsConnectionFailed": "接続に失敗しました", "generic": "エラー: {message}" } } diff --git a/src/locales/zh.json b/src/locales/zh.json index 02c8290..466952d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -15,7 +15,7 @@ "step3": "发送", "sendTitle": "开始发送", "encryptOn": "E2E加密 开启", - "maxConcurrent": "最大并发数", + "maxConcurrent": "最大并发数 (1-10)", "createRoom": "创建房间", "receiveTitle": "加入接收", "codePlaceholder": "输入代码(例如: ABCD...)", @@ -26,44 +26,76 @@ "room": { "eyebrow": "ROOM—SESSION", "roomLabel": "ROOM", - "copyLink": "LINK", - "copyCode": "CODE", - "roleLabel": "ROLE", - "peersLabel": "PEERS", - "encryptHint": "开启加密后,解密密钥在链接的\"#\"部分(不会发送到服务器)。", - "dropTitle": "DROP FILE HERE", + "copyLink": "复制链接", + "copyLinkHint": "复制完整URL(发送给对方)", + "copyCode": "复制代码", + "copyCodeHint": "仅复制房间ID", + "copied": "已复制!", + "roleLabel": "您的角色", + "peersLabel": "已连接", + "peersUnit": "人", + "maxConcurrentLimit": "最大并发: {max}人", + "encryptHint": "只有拥有链接的人才能接收文件", + "dropTitle": "将文件拖到这里", "dropOr": "或", - "select": "SELECT", - "send": "SEND", + "select": "选择文件", + "send": "发送", + "sendHintNoFile": "请选择文件", + "sendHintNoPeer": "等待对方连接", + "sendHintReady": "可以发送", "waiting": "等待发送方选择文件...", - "download": "DOWNLOAD" + "download": "下载" + }, + "guide": { + "stepLabel": "STEP {current} / {total}", + "senderShareLink": "将链接发送给对方", + "senderShareLinkSub": "复制链接或代码并分享", + "senderWaitPeer": "等待对方加入...", + "senderWaitPeerSub": "连接后即可开始传输", + "senderSelectFile": "选择要发送的文件", + "senderSelectFileSub": "拖放或点击选择", + "senderReadyToSend": "点击发送按钮", + "senderReadyToSendSub": "准备就绪!", + "senderSending": "发送中...", + "senderSendingSub": "正在传输文件", + "senderComplete": "发送完成!", + "senderCompleteSub": "文件已成功发送", + "receiverConnecting": "正在连接房间...", + "receiverConnectingSub": "请稍候", + "receiverWaitFile": "等待发送方...", + "receiverWaitFileSub": "发送方将选择文件", + "receiverReceiving": "接收中...", + "receiverReceivingSub": "下载将自动开始", + "receiverComplete": "准备下载!", + "receiverCompleteSub": "点击下方按钮" }, "status": { "initializing": "初始化中...", - "connecting": "正在连接信令服务器...", - "waitingSender": "作为发送方等待中...", - "waitingReceiver": "作为接收方等待中...", - "dataChannelReady": "数据通道就绪", - "dataChannelClosed": "数据通道已关闭", - "dataChannelError": "数据通道错误", - "generatingFile": "正在生成文件...", - "receiveComplete": "接收完成", + "connecting": "连接中...", + "waitingSender": "等待对方连接...", + "waitingSenderReady": "请选择文件并发送", + "waitingReceiver": "等待发送方...", + "dataChannelReady": "已连接!", + "dataChannelClosed": "连接已断开", + "dataChannelError": "连接错误", + "generatingFile": "准备文件中...", + "receiveComplete": "接收完成!", "sending": "发送中...", - "sendComplete": "发送完成", + "sendComplete": "发送完成!", "queued": "排队中...", "preparing": "准备连接中...", - "missingKey": "找不到加密密钥(需要 #k=...)", + "missingKey": "链接不完整(需要URL中#后的部分)", "receiving": "接收中: {name}", - "disconnected": "信令已断开" + "disconnected": "已断开" }, "role": { - "offerer": "发送方 (offerer)", - "answerer": "接收方 (answerer)", + "offerer": "发送方", + "answerer": "接收方", "unknown": "—" }, "error": { "roomCreationFailed": "创建房间失败", - "wsConnectionFailed": "WebSocket连接失败", + "wsConnectionFailed": "连接失败", "generic": "错误: {message}" } } diff --git a/src/room.ts b/src/room.ts index 7b1fcd5..11a66e2 100644 --- a/src/room.ts +++ b/src/room.ts @@ -33,6 +33,12 @@ type RoomConfig = { }; const DEFAULT_MAX_CONCURRENT = 3; +const MAX_MAX_CONCURRENT = 10; + +function normalizeMaxConcurrent(value?: number) { + const base = Number.isFinite(value) ? Math.floor(value!) : DEFAULT_MAX_CONCURRENT; + return Math.min(MAX_MAX_CONCURRENT, Math.max(1, base)); +} function toText(message: ArrayBuffer | string) { return typeof message === "string" ? message : new TextDecoder().decode(message); @@ -60,6 +66,7 @@ export class Room extends DurableObjectBase { // Design: README.md (multi-receiver queue); related: src/index.tsx routes and src/client/room.tsx signaling. private ctx: DurableObjectState; private config: RoomConfig | null = null; + private activePairs = new Map(); constructor(state: DurableObjectState, env: Bindings) { super(state, env); this.ctx = state; @@ -69,16 +76,19 @@ export class Room extends DurableObjectBase { const url = new URL(request.url); if (url.pathname === "/config") { - if (request.method !== "POST") { - return new Response("Expected POST", { status: 400 }); + if (request.method === "POST") { + const body = (await request.json()) as { maxConcurrent?: number; creatorCid?: string }; + const maxConcurrent = normalizeMaxConcurrent(body.maxConcurrent); + const creatorCid = typeof body.creatorCid === "string" ? body.creatorCid : undefined; + this.config = { maxConcurrent, creatorCid }; + await this.ctx.storage.put("config", this.config); + return new Response("OK"); + } + if (request.method === "GET") { + await this.ensureConfig(); + return Response.json(this.config ?? { maxConcurrent: DEFAULT_MAX_CONCURRENT }); } - const body = (await request.json()) as { maxConcurrent?: number; creatorCid?: string }; - const max = Number.isFinite(body.maxConcurrent) ? Math.floor(body.maxConcurrent!) : DEFAULT_MAX_CONCURRENT; - const maxConcurrent = Math.max(1, max); - const creatorCid = typeof body.creatorCid === "string" ? body.creatorCid : undefined; - this.config = { maxConcurrent, creatorCid }; - await this.ctx.storage.put("config", this.config); - return new Response("OK"); + return new Response("Expected POST or GET", { status: 400 }); } const upgrade = request.headers.get("Upgrade"); @@ -112,9 +122,9 @@ export class Room extends DurableObjectBase { this.ctx.acceptWebSocket(server); server.serializeAttachment(attachment); - server.send(JSON.stringify({ type: "role", role, cid: clientId } satisfies ServerToClient)); + this.sendJson(server, { type: "role", role, cid: clientId }); if (role === "answerer") { - server.send(JSON.stringify({ type: "wait" } satisfies ServerToClient)); + this.sendJson(server, { type: "wait" }); } this.broadcastPeers(); @@ -146,6 +156,23 @@ export class Room extends DurableObjectBase { } if (msg.type === "offer" || msg.type === "answer" || msg.type === "candidate") { + if (msg.type === "offer") { + if (attachment.role !== "offerer") return; + if (this.activePairs.get(msg.to) !== attachment.cid) return; + } + if (msg.type === "answer") { + if (attachment.role !== "answerer") return; + if (this.activePairs.get(attachment.cid) !== msg.to) return; + } + if (msg.type === "candidate") { + if (attachment.role === "offerer") { + if (this.activePairs.get(msg.to) !== attachment.cid) return; + } else if (attachment.role === "answerer") { + if (this.activePairs.get(attachment.cid) !== msg.to) return; + } else { + return; + } + } const target = this.socketByCid(msg.to); if (!target) return; @@ -156,7 +183,7 @@ export class Room extends DurableObjectBase { ? { type: "answer", from: attachment.cid, sid: msg.sid, sdp: msg.sdp } : { type: "candidate", from: attachment.cid, sid: msg.sid, candidate: msg.candidate }; - target.send(JSON.stringify(payload satisfies ServerToClient)); + this.sendJson(target, payload); } } @@ -165,17 +192,31 @@ export class Room extends DurableObjectBase { log("[room] webSocketClose, cid:", attachment?.cid, "role:", attachment?.role); if (attachment?.role === "answerer") { + const hasReplacement = attachment.cid && this.hasOpenSocket(attachment.cid, "answerer"); + if (hasReplacement) { + this.broadcastPeers(); + return; + } + if (attachment.cid) { + this.activePairs.delete(attachment.cid); + } const offerer = this.getOffererSocket(); if (offerer && attachment.cid) { - offerer.send(JSON.stringify({ type: "peer-left", peerId: attachment.cid } satisfies ServerToClient)); + this.sendJson(offerer, { type: "peer-left", peerId: attachment.cid }); } this.fillSlots(); } if (attachment?.role === "offerer") { + const hasReplacement = attachment.cid && this.hasOpenSocket(attachment.cid, "offerer"); + if (hasReplacement) { + this.broadcastPeers(); + return; + } + this.activePairs.clear(); for (const socket of this.answererSockets()) { this.setAnswererState(socket, "waiting"); - socket.send(JSON.stringify({ type: "wait" } satisfies ServerToClient)); + this.sendJson(socket, { type: "wait" }); } } @@ -190,7 +231,10 @@ export class Room extends DurableObjectBase { if (this.config) return this.config; const stored = await this.ctx.storage.get("config"); if (stored?.maxConcurrent) { - this.config = stored; + this.config = { + ...stored, + maxConcurrent: normalizeMaxConcurrent(stored.maxConcurrent), + }; } else { this.config = { maxConcurrent: DEFAULT_MAX_CONCURRENT }; } @@ -198,9 +242,9 @@ export class Room extends DurableObjectBase { } private broadcastPeers() { - const count = this.ctx.getWebSockets().length; - const payload = JSON.stringify({ type: "peers", count } satisfies ServerToClient); - for (const socket of this.ctx.getWebSockets()) socket.send(payload); + const sockets = this.openSockets(); + const payload = JSON.stringify({ type: "peers", count: sockets.length } satisfies ServerToClient); + for (const socket of sockets) this.sendText(socket, payload); } private pickRole(clientId: string) { @@ -214,7 +258,7 @@ export class Room extends DurableObjectBase { } private getOffererSocket() { - for (const socket of this.ctx.getWebSockets()) { + for (const socket of this.openSockets()) { const attachment = socket.deserializeAttachment() as SocketAttachment | null; if (attachment?.role === "offerer") return socket; } @@ -223,7 +267,7 @@ export class Room extends DurableObjectBase { private answererSockets() { const out: WebSocket[] = []; - for (const socket of this.ctx.getWebSockets()) { + for (const socket of this.openSockets()) { const attachment = socket.deserializeAttachment() as SocketAttachment | null; if (attachment?.role === "answerer") out.push(socket); } @@ -231,7 +275,7 @@ export class Room extends DurableObjectBase { } private socketByCid(cid: string) { - for (const socket of this.ctx.getWebSockets()) { + for (const socket of this.openSockets()) { const attachment = socket.deserializeAttachment() as SocketAttachment | null; if (attachment?.cid === cid) return socket; } @@ -248,6 +292,9 @@ export class Room extends DurableObjectBase { private fillSlots() { const offerer = this.getOffererSocket(); if (!offerer) return; + const offererAttachment = offerer.deserializeAttachment() as SocketAttachment | null; + const offererCid = offererAttachment?.cid; + if (!offererCid) return; const maxConcurrent = this.config?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT; @@ -275,18 +322,48 @@ export class Room extends DurableObjectBase { const attachment = socket.deserializeAttachment() as SocketAttachment | null; if (!attachment) continue; this.setAnswererState(socket, "active"); - socket.send(JSON.stringify({ type: "start" } satisfies ServerToClient)); - offerer.send(JSON.stringify({ type: "start", peerId: attachment.cid } satisfies ServerToClient)); + if (attachment.cid) { + this.activePairs.set(attachment.cid, offererCid); + } + this.sendJson(socket, { type: "start" }); + this.sendJson(offerer, { type: "start", peerId: attachment.cid }); } } - private closeDuplicateClient(clientId: string) { + private closeDuplicateClient(clientId: string, keepSocket?: WebSocket) { for (const socket of this.ctx.getWebSockets()) { + if (keepSocket && socket === keepSocket) continue; const attachment = socket.deserializeAttachment() as SocketAttachment | null; if (attachment?.cid === clientId) { log("[room] closing duplicate client, cid:", clientId, "role:", attachment.role); - socket.close(1000, "replaced"); + if (socket.readyState === WebSocket.OPEN) { + socket.close(1000, "replaced"); + } } } } + + private openSockets() { + return this.ctx.getWebSockets().filter((socket) => socket.readyState === WebSocket.OPEN); + } + + private hasOpenSocket(cid: string, role?: Role) { + for (const socket of this.openSockets()) { + const attachment = socket.deserializeAttachment() as SocketAttachment | null; + if (!attachment || attachment.cid !== cid) continue; + if (role && attachment.role !== role) continue; + return true; + } + return false; + } + + private sendJson(socket: WebSocket, payload: ServerToClient) { + if (socket.readyState !== WebSocket.OPEN) return; + socket.send(JSON.stringify(payload)); + } + + private sendText(socket: WebSocket, payload: string) { + if (socket.readyState !== WebSocket.OPEN) return; + socket.send(payload); + } } diff --git a/src/ui/layout.tsx b/src/ui/layout.tsx index b7bc420..bb8e9f9 100644 --- a/src/ui/layout.tsx +++ b/src/ui/layout.tsx @@ -40,12 +40,12 @@ export function Layout({ title, children, scripts, bodyAttrs = {}, t, locale }: {supportedLocales.map((l) => { const langT = getTranslations(l); const isActive = l === locale; - const basePath = typeof window !== "undefined" ? window.location.pathname : ""; return ( {l.toUpperCase()} diff --git a/src/ui/room.tsx b/src/ui/room.tsx index c1b33b5..2f4b1bb 100644 --- a/src/ui/room.tsx +++ b/src/ui/room.tsx @@ -9,16 +9,19 @@ import type { Translations, Locale } from "../i18n"; type RoomPageProps = { roomId: string; + maxConcurrent: number; t: Translations; locale: Locale; }; -export function RoomPage({ roomId, t, locale }: RoomPageProps) { +export function RoomPage({ roomId, maxConcurrent, t, locale }: RoomPageProps) { + const maxConcurrentLabel = t.room.maxConcurrentLimit.replace("{max}", String(maxConcurrent)); + const initialStepLabel = t.guide.stepLabel.replace("{current}", "1").replace("{total}", "4"); return ( } - bodyAttrs={{ "data-room-id": roomId }} + bodyAttrs={{ "data-room-id": roomId, "data-max-concurrent": String(maxConcurrent) }} t={t} locale={locale} > @@ -30,8 +33,8 @@ export function RoomPage({ roomId, t, locale }: RoomPageProps) {
{t.status.initializing}
- - + +
@@ -41,12 +44,25 @@ export function RoomPage({ roomId, t, locale }: RoomPageProps) {
{t.room.roleLabel}
{t.role.unknown}
{t.room.peersLabel}
-
0
+
0{t.room.peersUnit}
{t.room.encryptHint}
+
{maxConcurrentLabel}
+
+
{initialStepLabel}
+
{t.guide.receiverConnecting}
+
{t.guide.receiverConnectingSub}
+
+
+
+
+
+
+
+