diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d6dfc95d..31aaeed1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,19 +4,15 @@ updates: directory: / schedule: interval: weekly - # LiveKit's HTTP and WebSocket stack pins these majors; moving ahead of it - # builds each crate twice. Lift an entry when LiveKit moves. - ignore: - - dependency-name: reqwest - update-types: ["version-update:semver-major", "version-update:semver-minor"] - - dependency-name: tokio-tungstenite - update-types: ["version-update:semver-major", "version-update:semver-minor"] - - dependency-name: tower-http - update-types: ["version-update:semver-major", "version-update:semver-minor"] # Separate groups so a failure in one does not hold back the rest: a # LiveKit release can raise the Clang floor in scripts/fetch-clang.sh, and # a tree-sitter grammar has to stay within the runtime's ABI range and the # version named in docs/evidence-runtime.md. + # + # Security updates otherwise arrive one crate per pull request; only the + # coupled sets are grouped for them too. Nothing is ignored, because an + # ignore rule also suppresses security pull requests; a bump past a crate + # under vendor/ fails the vendor-patches gate in scripts/test.sh instead. groups: livekit: patterns: @@ -28,6 +24,15 @@ updates: rust: patterns: - "*" + livekit-security: + applies-to: security-updates + patterns: + - "livekit*" + - "webrtc-sys*" + tree-sitter-security: + applies-to: security-updates + patterns: + - "tree-sitter*" - package-ecosystem: npm directory: / schedule: diff --git a/Cargo.lock b/Cargo.lock index 17f4899a..d4b5ff7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "async-compression" -version = "0.4.48" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" +checksum = "9d5755640120b24961f8231e05892e4a4f7e0d04e8aeca161508c9c397946970" dependencies = [ "compression-codecs", "compression-core", @@ -59,7 +59,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -148,15 +148,6 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" -[[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-buffer" version = "0.12.1" @@ -209,9 +200,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.5.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" dependencies = [ "find-msvc-tools", "jobserver", @@ -244,7 +235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures", "rand_core 0.10.1", ] @@ -313,33 +304,38 @@ dependencies = [ "chrono", "futures-util", "hmac", + "hyper-util", "jpeg-encoder", "livekit", + "livekit-net", "livekit-protocol", - "reqwest", + "reqwest 0.13.5", "ring", "rusqlite", "rust-embed", + "rustls", "serde", "serde_json", "sha2", "tokio", "tokio-tungstenite", "tokio-util", - "tower-http", + "tower-http 0.7.1", + "tower-service", "tree-sitter", "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-java", "tree-sitter-javascript", "tree-sitter-python", + "webpki-roots", ] [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", @@ -347,9 +343,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.43" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" +checksum = "cae36683101b2899f48bd31f8db9839437356188e3b1ff23096e73dab09ba8fe" dependencies = [ "compression-core", "flate2", @@ -358,9 +354,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" +checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a" [[package]] name = "const-oid" @@ -384,15 +380,6 @@ 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 = "cpufeatures" version = "0.3.1" @@ -404,23 +391,13 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -432,18 +409,18 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] [[package]] name = "cxx" -version = "1.0.202" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f6de320895f42e6e081abb5c7983bedcf0b6d0ff9323de0d33f620c8ac1199" +checksum = "c8a02bfeba9074b2bbb39492c9b0908ec854f455a3cccdcde7a1de55e2315e40" dependencies = [ "cc", "cxx-build", @@ -456,9 +433,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.202" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fde53ca86b9704a943fef0f1e1d836239a6aedca5de3c65fa9f97ec0bd46d39" +checksum = "bbc2ac76997619d2d888b58ad79488f1c835de41ca4dd3dd7082acf4ecb0a50f" dependencies = [ "cc", "codespan-reporting", @@ -466,39 +443,39 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.202" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bae89236c811fd4d08ed3441759ac4ac98d752cbfd3de341315ba16ad20ec3" +checksum = "54a486ed16a3f2a053fe7f5b98db4c1a59846a23e8cb9e49f9d1bd48830d51a2" dependencies = [ "clap", "codespan-reporting", "indexmap", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "cxxbridge-flags" -version = "1.0.202" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49045042e5fced01b80742aba5508de82aa4f13677ed3fa2b4cda709c40c5918" +checksum = "c72569be94e2283764d334cd285980b747dbe221e27e315484eb6bcd93fa808a" [[package]] name = "cxxbridge-macro" -version = "1.0.202" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b181252e2e3d3b5d183afbdc033a3958b445eb3e1a0ae65fc3d4f5259f5da6fd" +checksum = "423e795284a7f7b3f843c956a5322c4bc497b60c2dd1a6339da10313cb18d575" dependencies = [ "indexmap", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -549,7 +526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ca8e71544c1b67dcdbc2699ab258828aff985e5bc8d5f6b486d90d7df2f848" dependencies = [ "core-foundation", - "jni", + "jni 0.21.1", "libc", "thiserror 2.0.21", "wasm-bindgen", @@ -557,25 +534,15 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", "ctutils", ] @@ -597,20 +564,20 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" @@ -642,9 +609,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "fixedbitset" @@ -770,16 +737,6 @@ dependencies = [ "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", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -865,7 +822,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -924,13 +881,12 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", - "futures-channel", "futures-core", "http", "http-body", @@ -957,7 +913,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.9", + "webpki-roots", ] [[package]] @@ -1165,6 +1121,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1222,15 +1208,15 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libloading" @@ -1255,12 +1241,12 @@ dependencies = [ [[package]] name = "libwebrtc" -version = "0.3.52" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8718693f33f921a97c784f34fcbf085ebe68989e53bf4802f93aa8e083cf0a65" +checksum = "30c02c77bc04e81cd4b7415b6485abe249bc38979f41808e6496633712e90e55" dependencies = [ "cxx", - "jni", + "jni 0.21.1", "js-sys", "lazy_static", "log", @@ -1300,9 +1286,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "livekit" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b997899186e33ae91e6c87b9d3887632dd7ca5ba3fe02c0ebf0171974899571c" +checksum = "7361f65ace7c7b4b0fdbaed46c3738075de6dece1fd715e05347d35d655d0af1" dependencies = [ "bmrng", "bytes", @@ -1329,18 +1315,18 @@ dependencies = [ [[package]] name = "livekit-common" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41d06b9a8c37ad2691faabc091394101c876c2eb8ab1e61b3b021b4d5aacfd4" +checksum = "728a0e2cc2991a3392e2a1c9321b92f3f49d35f02ffc3e071ec75ea66fa9a5bc" dependencies = [ "livekit-protocol", ] [[package]] name = "livekit-data-stream" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e424524e3a8278e5a5d498e18817c017df922dae37551943e3afbf1458843e88" +checksum = "e47c0849044eb3efa012d5263ff32d6d64300667fa2d5c7d6c5cda4bb2cfb966" dependencies = [ "async-compression", "bmrng", @@ -1361,9 +1347,9 @@ dependencies = [ [[package]] name = "livekit-datatrack" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ea81d9e992be78a6089a2700cba7a2225bf3ebb9b821d1b9479a59537d9341" +checksum = "a610cb4b2a6927fb21dee3d089bfb5d39da1b71de383201a89b7ac9bf80e1c74" dependencies = [ "anyhow", "bytes", @@ -1380,27 +1366,26 @@ dependencies = [ [[package]] name = "livekit-net" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9528a26c807f49d3fa6d075c01db5fd77bf32454b1e700c530fb8c670892008" +version = "0.1.4" dependencies = [ "async-trait", "base64 0.21.7", - "bytes", "futures-util", "http", "log", - "reqwest", + "reqwest 0.12.28", "tokio", + "tokio-rustls", "tokio-tungstenite", "url", + "webpki-roots", ] [[package]] name = "livekit-protocol" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39029b01173f266fbb60a7f546616cef882427e32452594030868df585970024" +checksum = "1329f59d49f876eb15aae91b1a6bb72b8d75ea4f9eceba606191af468c2c231d" dependencies = [ "pbjson", "pbjson-types", @@ -1419,9 +1404,9 @@ dependencies = [ [[package]] name = "livekit-rpc" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d948a38a139c142aa3c42d3679b3f39d04d3e07981e4de5b16bae21b1eb50189" +checksum = "71d9d843d1354aafb39c4f9fd3481adcebbcde730f9b8ce247fde4b5330a7a5c" dependencies = [ "livekit-common", "livekit-data-stream", @@ -1436,9 +1421,9 @@ dependencies = [ [[package]] name = "livekit-signaling" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3867060e1f1e7073f367c218688042fc014f8b8ebd65749e411400ad9b7e3a" +checksum = "52fb8712633820ca0d186fadd7ca76c373d1cb3b555d143d7fee232b2493794b" dependencies = [ "base64 0.21.7", "device-info", @@ -1519,9 +1504,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi", @@ -1557,9 +1542,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", ] @@ -1941,9 +1926,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "bytes", "getrandom 0.4.3", @@ -1963,9 +1948,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ "cfg_aliases", "libc", @@ -2121,13 +2106,50 @@ dependencies = [ "tokio", "tokio-rustls", "tower", - "tower-http", + "tower-http 0.6.11", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.9", ] [[package]] @@ -2205,6 +2227,15 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.5" @@ -2224,6 +2255,7 @@ version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2234,9 +2266,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2254,11 +2286,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -2288,9 +2347,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" dependencies = [ "windows-sys 0.61.2", ] @@ -2363,7 +2422,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2406,13 +2465,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.7" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -2422,8 +2481,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -2448,6 +2507,22 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -2456,9 +2531,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "socket2" @@ -2524,9 +2599,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -2550,7 +2625,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2611,7 +2686,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2632,9 +2707,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" dependencies = [ "bytes", "libc", @@ -2654,14 +2729,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -2681,9 +2756,7 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +version = "0.30.0" dependencies = [ "futures-util", "log", @@ -2692,7 +2765,7 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] @@ -2728,21 +2801,37 @@ name = "tower-http" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-http" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" dependencies = [ "async-compression", "bitflags", "bytes", "futures-core", - "futures-util", "http", "http-body", + "percent-encoding", "pin-project-lite", "tokio", "tokio-util", - "tower", "tower-layer", "tower-service", - "url", ] [[package]] @@ -2853,16 +2942,16 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.9.5", + "rand 0.10.3", "rustls", "rustls-pki-types", "sha1", @@ -2872,9 +2961,9 @@ dependencies = [ [[package]] name = "typed-path" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c" [[package]] name = "typenum" @@ -2884,15 +2973,15 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-width" @@ -2926,9 +3015,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.26.1" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -2941,12 +3030,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -2959,9 +3042,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -3024,7 +3107,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -3058,28 +3141,28 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.11" +name = "webpki-root-certs" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ - "webpki-roots 1.0.9", + "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] [[package]] name = "webrtc-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b803a52c561fbb9faaf09c1b6fc0260f337cbc1ae6f2a525f673c77eb4411022" +checksum = "f6fab7306a96fa68e97cb929ca0a9592ac9ab27b350f961f2d35d29ff8af89a1" dependencies = [ "cc", "cxx", @@ -3092,12 +3175,14 @@ dependencies = [ [[package]] name = "webrtc-sys-build" -version = "0.3.19" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294bd0dfb00120269cdb4d71371348361daa454a6a31c8dd035dfe2a2be47871" dependencies = [ "anyhow", "fs2", "regex", - "reqwest", + "reqwest 0.12.28", "scratch", "semver", "zip", @@ -3381,24 +3466,24 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.59" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.59" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -3422,15 +3507,15 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zerotrie" @@ -3462,14 +3547,14 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "zip" -version = "9.0.4" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "crc32fast", "flate2", diff --git a/Cargo.toml b/Cargo.toml index bebe1ce8..2c68304a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,26 +22,36 @@ all = "deny" axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "original-uri", "query", "tokio"] } base64 = "0.23.1" futures-util = { version = "0.3.34", default-features = false, features = ["sink", "std"] } +# reqwest's proxy matcher and connectors, so the Gemini Live socket follows +# the same HTTPS_PROXY and NO_PROXY rules as every HTTP call. reqwest already +# builds these features. +hyper-util = { version = "0.1.21", default-features = false, features = ["client-legacy", "client-proxy", "tokio"] } jpeg-encoder = "0.7.1" -livekit = { version = "0.9.3", features = ["rustls-tls-webpki-roots"] } +livekit = { version = "0.9.6", features = ["rustls-tls-webpki-roots"] } ring = "0.17.14" -# Mozilla's roots compiled in, for this client and the Gemini WebSocket below -# alike, so the binary verifies every host it talks to without asking the -# machine for a CA store: docs/install.md promises nothing is required beyond -# the binary. The trade is that a CA an organization installs locally, a -# TLS-inspecting proxy's for one, is not trusted. +# Mozilla's roots compiled in, one rustls configuration shared by this HTTP +# client and the Gemini WebSocket, so the binary verifies every host it talks +# to without asking the machine for a CA store: docs/install.md promises +# nothing is required beyond the binary. The trade is that a CA an +# organization installs locally, a TLS-inspecting proxy's for one, is not +# trusted. # -# reqwest, tokio-tungstenite and tower-http stay on the majors LiveKit depends -# on, so each builds once; .github/dependabot.yml holds them there. -reqwest = { version = "0.12.28", default-features = false, features = ["json", "rustls-tls"] } +# Direct dependencies track their latest release so Dependabot can deliver +# security fixes. That builds two majors of reqwest (LiveKit is on 0.12) and of +# tower-http (reqwest 0.13 is on 0.6); they collapse when LiveKit and reqwest +# move, and pinning back here would also block their security updates. +reqwest = { version = "0.13.5", default-features = false, features = ["json", "form", "query", "rustls-no-provider"] } +rustls = { version = "0.23.46", default-features = false, features = ["ring", "std", "tls12"] } +webpki-roots = "1.0.10" rusqlite = { version = "0.40.2", default-features = false, features = ["bundled"] } -rust-embed = { version = "8.12.0", features = ["include-exclude"] } -serde_json = "1.0.151" +rust-embed = { version = "8.13.0", features = ["include-exclude"] } +serde_json = "1.0.154" serde = { version = "1.0.229", features = ["derive"] } -tokio = { version = "1.53.1", features = ["fs", "macros", "net", "rt-multi-thread", "sync", "time"] } -tokio-tungstenite = { version = "0.29.0", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] } -tokio-util = "0.7.19" -tower-http = { version = "0.6.11", default-features = false, features = ["compression-gzip"] } +tokio = { version = "1.54.0", features = ["fs", "macros", "net", "rt-multi-thread", "sync", "time"] } +tokio-tungstenite = { version = "0.30.0", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] } +tokio-util = "0.7.20" +tower-http = { version = "0.7.1", default-features = false, features = ["compression-gzip"] } +tower-service = "0.3.3" tree-sitter = "=0.27.1" tree-sitter-c = "=0.24.2" tree-sitter-cpp = "=0.23.4" @@ -60,12 +70,14 @@ tree-sitter-python = "=0.25.0" [dev-dependencies] # Independent implementations verify production signatures and digest fixtures. hmac = "0.13.0" -sha2 = "0.11.0" +sha2 = "0.11.1" chrono = { version = "0.4.45", default-features = false, features = ["std"] } -livekit-protocol = "0.8.0" +livekit-protocol = "0.8.2" +# LiveKit's transport, reached directly to check the vendored proxy path. +livekit-net = "0.1.4" # The paused clock, so the Live socket's keepalive and idle limits are tested # at their real values instead of waiting them out. -tokio = { version = "1.53.1", features = ["test-util"] } +tokio = { version = "1.54.0", features = ["test-util"] } # Release archives distribute the executable, not a debugging target. Cargo # already omits debug information in this profile; omit linker symbols too. @@ -131,7 +143,8 @@ debug-assertions = true [profile.test.package.rust-embed] debug-assertions = true -# Moves LiveKit's build helper off vulnerable zip 0.6. See -# vendor/webrtc-sys-build/README.md for what changed and when to remove it. +# Move both WebSocket callers to one transport with patched certificate roots. +# Each vendor README records the upstream condition for removing its patch. [patch.crates-io] -webrtc-sys-build = { path = "vendor/webrtc-sys-build" } +livekit-net = { path = "vendor/livekit-net" } +tokio-tungstenite = { path = "vendor/tokio-tungstenite" } diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 4ec5dc5b..c402ed86 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -35,11 +35,19 @@ License texts and source details are shipped with the relevant assets: - `web/vendor/pyodide/README.md` - `web/vendor/pdfjs/README.md` -The build also patches one Rust crate, kept in source form with its license: - -- `vendor/webrtc-sys-build/` ([webrtc-sys-build](https://crates.io/crates/webrtc-sys-build) - 0.3.19, Apache-2.0), LiveKit's libwebrtc download helper, moved to zip 9; - its `README.md` and `LICENSE` say what changed +The build also patches two Rust crates, kept in source form with their licenses: + +- `vendor/livekit-net/` ([livekit-net](https://crates.io/crates/livekit-net) + 0.1.4, Apache-2.0), LiveKit's network transport, moved to tokio-tungstenite + 0.30 for patched certificate roots and given bundled roots on its proxied + path; its `README.md` and `LICENSE` say what changed +- `vendor/tokio-tungstenite/` ([tokio-tungstenite](https://crates.io/crates/tokio-tungstenite) + 0.30.0, MIT), moved to webpki-roots 1.0.10; its `README.md` and `LICENSE` + say what changed + +The executable compiles in Mozilla's root certificates from +[webpki-roots](https://crates.io/crates/webpki-roots) 1.0.10, under +the [Community Data License Agreement - Permissive 2.0](https://cdla.dev/permissive-2-0/). Compiler Explorer is a remote service, not bundled software. C, C++, and Java source is sent to it only when remote test runs are enabled. diff --git a/docs/install.md b/docs/install.md index 8c73b4a0..7e7a2d3e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -28,6 +28,16 @@ upstream URL, checks it against a pinned SHA-256, and keeps it in its own cache. Without that reachable, the interview falls back to Jim's voice-only panel and nothing else changes. +On a network that reaches the internet only through a proxy, set `HTTPS_PROXY` +to an `http://` proxy URL. The binary's HTTP requests, the Gemini Live socket +and LiveKit signaling go through it; `NO_PROXY` exempts hosts from all but +LiveKit signaling, which LiveKit's transport proxies regardless. LiveKit's audio +and video travel over WebRTC, which does not use the proxy, and the browser +fetches the avatar model through its own proxy settings, not this one. +Certificates are still checked against Mozilla's roots compiled into the binary, +so a TLS-inspecting proxy, whose certificate authority is installed only on the +machine, is refused. + ```bash # Linux tar -xzf codetrial-x86_64-unknown-linux-gnu.tar.gz diff --git a/scripts/test.sh b/scripts/test.sh index edb72d0a..34b77ce2 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -264,6 +264,30 @@ cargo_audit_gate() cargo audit --file "$ROOT/Cargo.lock" } +# The crates under vendor/ carry fixes upstream has not released. A bump that +# outruns a vendored copy brings the registry crate back beside it, or in its +# place with only a Cargo warning, so no vendored name may resolve from a +# registry. +vendor_patches_gate() +{ + # One pass, the manifests before the lockfile: awk -v cannot carry a + # multi-line list portably. The [package] name only: [lib], [[test]] and + # [[example]] have names too. Deduplicated in awk rather than by a pipe, so + # an unreadable input fails the gate instead of passing it empty. + unpatched=$(awk -v lock="$ROOT/Cargo.lock" ' + FILENAME != lock && /^\[/ { package = ($0 == "[package]") } + FILENAME != lock && package && /^name = / { gsub(/"/, "", $3); vendored[$3] = 1 } + FILENAME == lock && /^name = / { name = $3; gsub(/"/, "", name) } + FILENAME == lock && /^source = "registry\+/ && (name in vendored) && !seen[name]++ { print name } + ' "$ROOT"/vendor/*/Cargo.toml "$ROOT/Cargo.lock") || return 1 + if [ -n "$unpatched" ]; then + echo "vendored crates also resolve from a registry; re-vendor them from" >&2 + echo "the version Cargo.lock now wants (see vendor/*/README.md):" >&2 + echo "$unpatched" >&2 + return 1 + fi +} + # The Python here is not incidental: it generates the problem bank, checks the # study plan and drives two integration harnesses. It had unittest suites and no # linter, which is how an unused import or a name that does not exist survives @@ -401,6 +425,7 @@ gate latency-tests unittest_gate "$ROOT/tests/test_analyze_latency.py" gate --lane browser browser-tests browser_tests static gate eslint eslint_gate gate cargo-audit cargo_audit_gate +gate vendor-patches vendor_patches_gate gate actionlint actionlint_gate gate browser-check-syntax node --check "$ROOT/scripts/browser-check.cjs" diff --git a/src/delivery.rs b/src/delivery.rs index 98c27328..1dc2c73c 100644 --- a/src/delivery.rs +++ b/src/delivery.rs @@ -159,7 +159,7 @@ impl GoogleDelivery { let credentials = parse_service_account(service_account_json)?; let fields = service_account_fields(&credentials)?; Ok(Self { - http: reqwest::Client::builder() + http: crate::http_client_builder() .timeout(REQUEST_TIMEOUT) .build() .map_err(|error| format!("the delivery client could not be built: {error}"))?, diff --git a/src/gemini.rs b/src/gemini.rs index 15e15ba0..02470cdf 100644 --- a/src/gemini.rs +++ b/src/gemini.rs @@ -8,9 +8,7 @@ use serde_json::{Value, json}; use tokio::net::TcpStream; use tokio::sync::mpsc::{Receiver, channel}; use tokio::task::JoinHandle; -use tokio_tungstenite::{ - MaybeTlsStream, WebSocketStream, connect_async, tungstenite::protocol::Message, -}; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::protocol::Message}; use crate::agent::InterviewMode; use crate::percent_encode_component; @@ -20,6 +18,7 @@ use crate::runtime::{ }; mod credentials; +mod socket; pub use credentials::GeminiKeys; /// What every image this process sends Gemini is encoded as: a camera frame, a @@ -1653,7 +1652,7 @@ async fn open_live_session_redacted_at( resume: Option<&str>, api_keys: Vec, ) -> Result> { - let (mut stream, _) = tokio::time::timeout(CONNECT_TIMEOUT, connect_async(url)) + let mut stream = tokio::time::timeout(CONNECT_TIMEOUT, socket::connect(url)) .await .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "Gemini connect timed out"))??; stream diff --git a/src/gemini/socket.rs b/src/gemini/socket.rs new file mode 100644 index 00000000..255fdc25 --- /dev/null +++ b/src/gemini/socket.rs @@ -0,0 +1,73 @@ +//! Opens the Gemini Live socket, through a proxy when the environment names +//! one. reqwest already honors `HTTPS_PROXY`, `ALL_PROXY` and `NO_PROXY` for +//! every HTTP call; this applies the same rules through the same matcher and +//! CONNECT tunnel. Otherwise, on a network that only lets traffic out through a +//! proxy, the report would work and the live interview would not. +//! +//! Only `wss://` is proxied, as reqwest proxies only `https://` through +//! `HTTPS_PROXY`. A plain `ws://` URL is a loopback fixture, which a proxy +//! configured for the machine would receive and usually fail. + +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::client::legacy::connect::proxy::Tunnel; +use hyper_util::client::proxy::matcher::{Intercept, Matcher}; +use tokio::net::TcpStream; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::http::Uri; +use tokio_tungstenite::{ + Connector, MaybeTlsStream, WebSocketStream, client_async_tls_with_config, + connect_async_tls_with_config, +}; +use tower_service::Service; + +pub(super) type Socket = WebSocketStream>; +type BoxError = Box; + +pub(super) async fn connect(url: &str) -> Result { + let request = url.into_client_request()?; + let connector = Some(Connector::Rustls(crate::tls_config().clone())); + let Some(proxy) = proxy_for(request.uri(), &Matcher::from_env()) else { + let (socket, _) = connect_async_tls_with_config(request, None, false, connector).await?; + return Ok(socket); + }; + let stream = tunnel(&proxy, request.uri()).await?; + let (socket, _) = client_async_tls_with_config(request, stream, None, connector).await?; + Ok(socket) +} + +fn proxy_for(uri: &Uri, proxies: &Matcher) -> Option { + if uri.scheme_str() != Some("wss") { + return None; + } + // The matcher keys on the HTTP scheme the socket upgrades from. + let https = Uri::builder() + .scheme("https") + .authority(uri.authority()?.clone()) + .path_and_query("/") + .build() + .ok()?; + proxies.intercept(&https) +} + +/// A TCP stream tunneled with CONNECT through `proxy` to `target`'s host, +/// ready for TLS. Only an `http://` proxy, as reqwest here is built without +/// SOCKS support, so a SOCKS proxy would fail every HTTP call anyway. +async fn tunnel(proxy: &Intercept, target: &Uri) -> Result { + let uri = proxy.uri(); + if uri.scheme_str() != Some("http") { + return Err(format!( + "the Gemini Live socket goes through an http:// proxy, not {}://", + uri.scheme_str().unwrap_or("") + ) + .into()); + } + let mut connector = Tunnel::new(uri.clone(), HttpConnector::new()); + if let Some(auth) = proxy.basic_auth() { + connector = connector.with_auth(auth.clone()); + } + Ok(connector.call(target.clone()).await?.into_inner()) +} + +#[cfg(test)] +#[path = "../../tests/unit/gemini/socket.rs"] +mod tests; diff --git a/src/lib.rs b/src/lib.rs index 78da8c9c..465d1f4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,8 +130,46 @@ pub(crate) fn no_redirect_http_client() -> &'static reqwest::Client { // Built and `expect`ed rather than `unwrap_or_default()`: that hands back a // client with no timeout at all, quietly dropping the backstop this exists for. -fn http_client_builder() -> reqwest::ClientBuilder { - reqwest::Client::builder().timeout(std::time::Duration::from_secs(30)) +pub(crate) fn http_client_builder() -> reqwest::ClientBuilder { + bundled_roots_client_builder().timeout(std::time::Duration::from_secs(30)) +} + +/// The one TLS configuration for every connection this crate opens, HTTP and +/// the Gemini WebSocket alike: ring, and only the Mozilla roots compiled into +/// the binary, never the machine's certificate store. +pub(crate) fn tls_config() -> &'static std::sync::Arc { + static CONFIG: std::sync::OnceLock> = + std::sync::OnceLock::new(); + CONFIG.get_or_init(|| { + std::sync::Arc::new( + rustls::ClientConfig::builder_with_provider(std::sync::Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .expect("default TLS versions should be supported") + .with_root_certificates(rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }) + .with_no_client_auth(), + ) + }) +} + +/// A client builder on [`tls_config`], with no timeout. Without it reqwest 0.13 +/// fails to build a client on a host with no certificate store. Use +/// [`http_client`] unless a caller needs its own bounds. +pub fn bundled_roots_client_builder() -> reqwest::ClientBuilder { + // reqwest takes an owned config; the clone shares the root store. + reqwest::Client::builder().tls_backend_preconfigured(rustls::ClientConfig::clone(tls_config())) +} + +/// Makes ring the process-wide rustls provider for LiveKit's transport, which +/// builds its TLS client without one. rustls otherwise picks a provider from +/// crate features, and panics once a dependency enables a second. Called at +/// startup, and again where a room is joined for hosts that skip `main`. +pub fn install_crypto_provider() { + // An error only means a provider is already installed. + let _ = rustls::crypto::ring::default_provider().install_default(); } #[cfg(test)] diff --git a/src/livekit.rs b/src/livekit.rs index fbbb2ee8..03b300bf 100644 --- a/src/livekit.rs +++ b/src/livekit.rs @@ -3019,6 +3019,7 @@ async fn join_room( options.data_stream = options .data_stream .with_max_payload_byte_length(MAX_BOARD_BYTES); + crate::install_crypto_provider(); let (room, events) = Room::connect(&config.livekit_url, &token, options).await?; eprintln!( "joined room={} identity={}", diff --git a/src/main.rs b/src/main.rs index d6cabe3f..232c12a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ const MODES: [(&str, usize, &str, ModeFn); 3] = [ type ModeFn = fn(&[String], CliOptions) -> Result<(), String>; fn main() { + codetrial::install_crypto_provider(); let args = std::env::args().skip(1).collect::>(); std::process::exit(run_agent_command(&args)); } diff --git a/tests/common/http.rs b/tests/common/http.rs index 90c0dfe5..7c19e24e 100644 --- a/tests/common/http.rs +++ b/tests/common/http.rs @@ -11,7 +11,8 @@ use std::time::Duration; const FIXTURE_TIMEOUT: Duration = Duration::from_secs(60); pub fn client_builder() -> reqwest::ClientBuilder { - reqwest::Client::builder().no_proxy() + // The production trust store; see `bundled_roots_client_builder`. + codetrial::bundled_roots_client_builder().no_proxy() } pub fn client() -> reqwest::Client { diff --git a/tests/interview_behavior.rs b/tests/interview_behavior.rs index 048bd5e0..3c78b94f 100644 --- a/tests/interview_behavior.rs +++ b/tests/interview_behavior.rs @@ -369,6 +369,15 @@ fn gemini_shaped(chat: &Value) -> Value { json!({ "candidates": [{ "content": { "role": "model", "parts": parts } }] }) } +/// No 30-second backstop: a local model on a CPU, or a long judge call, can +/// take minutes to answer. Built per use, since a pooled connection would +/// outlive the test runtime that opened it. +fn live_client() -> reqwest::Client { + codetrial::bundled_roots_client_builder() + .build() + .expect("live HTTP client builds") +} + struct Conversation { client: reqwest::Client, backend: Backend, @@ -890,7 +899,7 @@ async fn uncertain_speech_is_clarified_without_crediting_or_correcting_it() { ), ] { let mut conversation = Conversation { - client: reqwest::Client::new(), + client: live_client(), backend: backend.clone(), instructions: build_instructions_for_plan( problem, @@ -994,7 +1003,7 @@ async fn live_interviewer_poses_the_variant_and_serves_hints_in_order() { for mode in [InterviewMode::Coding, InterviewMode::Whiteboard] { for problem in problems.iter().copied() { let mut conversation = Conversation { - client: reqwest::Client::new(), + client: live_client(), backend: backend.clone(), instructions: build_instructions_for_plan( problem, @@ -1152,7 +1161,7 @@ impl Candidate { problem.variant().brief_text() ); Candidate { - client: reqwest::Client::new(), + client: live_client(), base: candidate_base(), messages: vec![json!({ "role": "system", "content": system })], } @@ -1487,7 +1496,7 @@ async fn played_candidates_are_held_to_the_same_rules() { }, ); let mut conversation = Conversation { - client: reqwest::Client::new(), + client: live_client(), backend: backend.clone(), instructions: instructions.clone(), contents: Vec::new(), @@ -1549,7 +1558,7 @@ async fn played_candidates_are_held_to_the_same_rules() { async fn conversational_reacto_answers_are_recorded_in_the_same_reply() { let problem = get_problem(Some("two-sum")); let mut conversation = Conversation { - client: reqwest::Client::new(), + client: live_client(), backend: Backend::from_env(), instructions: build_instructions_for_plan( problem, @@ -1616,7 +1625,7 @@ async fn judge_phases(state: &mut RuntimeState, problem: &'static Problem) -> Ve .ok() .filter(|model| !model.is_empty()) .unwrap_or_else(|| codetrial::config::DEFAULT_GEMINI_REPORT_MODEL.to_string()); - let response: Value = reqwest::Client::new() + let response: Value = live_client() .post(format!( "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent" )) diff --git a/tests/unit/delivery.rs b/tests/unit/delivery.rs index f38509bb..475fc2ec 100644 --- a/tests/unit/delivery.rs +++ b/tests/unit/delivery.rs @@ -162,7 +162,7 @@ fn service_account_validation_agrees_at_configuration_and_startup() { /// Nothing here signs, so the credential only has to exist. fn delivery_reading_from(base: String) -> GoogleDelivery { GoogleDelivery { - http: reqwest::Client::builder().no_proxy().build().unwrap(), + http: crate::http_client_builder().no_proxy().build().unwrap(), credentials: ServiceAccount { client_email: String::new(), private_key: Vec::new(), diff --git a/tests/unit/gemini.rs b/tests/unit/gemini.rs index c94fb993..ddb6d112 100644 --- a/tests/unit/gemini.rs +++ b/tests/unit/gemini.rs @@ -2599,7 +2599,13 @@ async fn only_transient_upstream_failures_are_retried() { let _connection = listener.accept().await.unwrap(); tokio::time::sleep(Duration::from_secs(1)).await; }); - let timeout = reqwest::Client::new() + + // Built here rather than the shared client, whose pool would outlive this + // test's runtime; no proxy, so the stall is the one this test set up. + let timeout = crate::http_client_builder() + .no_proxy() + .build() + .unwrap() .get(format!("http://{address}")) .timeout(Duration::from_millis(10)) .send() diff --git a/tests/unit/gemini/socket.rs b/tests/unit/gemini/socket.rs new file mode 100644 index 00000000..69229808 --- /dev/null +++ b/tests/unit/gemini/socket.rs @@ -0,0 +1,105 @@ +//! The `tests` module of `src/gemini/socket.rs`, which declares this file by +//! path. A TLS server here cannot carry a certificate the bundled roots trust, +//! so the tunnel is checked under a plain WebSocket and the proxy choice on the +//! `wss://` URL apart from it. + +use super::*; +use futures_util::{SinkExt, StreamExt}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpListener; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::{accept_async, client_async}; + +fn proxies(proxy: &str) -> Matcher { + Matcher::builder() + .https(proxy) + .no("excluded.example") + .build() +} + +fn intercept(proxy: &str) -> Intercept { + proxy_for(&"wss://live.example/ws".parse().unwrap(), &proxies(proxy)).unwrap() +} + +/// A proxy that records the request head, answers with `reply`, and on a +/// 200 serves the WebSocket itself, echoing one message. +async fn proxy_fixture(reply: &'static str) -> (String, tokio::task::JoinHandle>) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + let (socket, _) = listener.accept().await.unwrap(); + let mut reader = BufReader::new(socket); + let mut head = Vec::new(); + loop { + let mut line = String::new(); + reader.read_line(&mut line).await.unwrap(); + if line == "\r\n" { + break; + } + head.push(line.trim_end().to_string()); + } + let mut socket = reader.into_inner(); + socket.write_all(reply.as_bytes()).await.unwrap(); + if reply.starts_with("HTTP/1.1 200") { + let mut socket = accept_async(socket).await.unwrap(); + let message = socket.next().await.unwrap().unwrap(); + socket.send(message).await.unwrap(); + } + head + }); + (format!("http://user:secret@{address}"), server) +} + +#[test] +fn only_secure_sockets_outside_no_proxy_are_proxied() { + let proxies = proxies("http://127.0.0.1:3128"); + let chosen = |url: &str| proxy_for(&url.parse().unwrap(), &proxies); + let proxy = chosen("wss://generativelanguage.googleapis.com/ws/live?key=k").unwrap(); + assert_eq!(proxy.uri().authority().unwrap().as_str(), "127.0.0.1:3128"); + assert!(chosen("wss://excluded.example/ws").is_none()); + assert!(chosen("ws://127.0.0.1:9000/ws").is_none()); +} + +#[tokio::test] +async fn the_tunnel_carries_the_socket_after_an_authorized_connect() { + let (proxy, server) = proxy_fixture("HTTP/1.1 200 Connection established\r\n\r\n").await; + let target = "wss://live.example/ws".parse().unwrap(); + let stream = tunnel(&intercept(&proxy), &target).await.unwrap(); + let (mut socket, _) = client_async("ws://live.example/ws", stream).await.unwrap(); + socket.send(Message::Text("ping".into())).await.unwrap(); + assert_eq!( + socket.next().await.unwrap().unwrap(), + Message::Text("ping".into()) + ); + let head = server.await.unwrap(); + assert_eq!(head[0], "CONNECT live.example:443 HTTP/1.1"); + assert!(head.contains(&"Host: live.example:443".to_string())); + // user:secret + assert!(head.contains(&"Proxy-Authorization: Basic dXNlcjpzZWNyZXQ=".to_string())); +} + +#[tokio::test] +async fn a_refused_tunnel_says_why() { + for (reply, reason) in [ + ( + "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n", + "proxy authorization required", + ), + ("HTTP/1.1 403 Forbidden\r\n\r\n", "unsuccessful"), + ] { + let (proxy, server) = proxy_fixture(reply).await; + let target = "wss://live.example/ws".parse().unwrap(); + let error = tunnel(&intercept(&proxy), &target).await.unwrap_err(); + assert!(error.to_string().contains(reason), "{error}"); + server.await.unwrap(); + } +} + +#[tokio::test] +async fn an_unsupported_proxy_is_refused_before_connecting() { + let target = "wss://live.example/ws".parse().unwrap(); + let error = tunnel(&intercept("https://127.0.0.1:1"), &target) + .await + .unwrap_err(); + assert!(error.to_string().contains("not https://"), "{error}"); +} diff --git a/tests/unit/lib.rs b/tests/unit/lib.rs index 4e9e0608..1a099cd7 100644 --- a/tests/unit/lib.rs +++ b/tests/unit/lib.rs @@ -46,3 +46,47 @@ fn shared_http_clients_carry_the_backstop_timeout() { assert!(debug.contains("TotalTimeout: 30s"), "{debug}"); } } + +/// LiveKit's transport builds its TLS client on the process-wide provider, so +/// this has to leave ring installed. Checked in a child, because any test in +/// this process that joined a room may have installed one already. +#[test] +fn install_crypto_provider_makes_ring_the_process_default() { + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "tests::install_crypto_provider_child", + "--nocapture", + "--test-threads=1", + ]) + .env("CODETRIAL_CRYPTO_PROVIDER_TEST_CHILD", "1") + .output() + .unwrap(); + assert!( + output.status.success(), + "child failed: {}", + String::from_utf8_lossy(&output.stdout) + ); + assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed")); +} + +#[test] +fn install_crypto_provider_child() { + if std::env::var_os("CODETRIAL_CRYPTO_PROVIDER_TEST_CHILD").is_none() { + return; + } + assert!(rustls::crypto::CryptoProvider::get_default().is_none()); + super::install_crypto_provider(); + let installed = rustls::crypto::CryptoProvider::get_default() + .expect("a provider is installed") + .cipher_suites + .iter() + .map(|suite| suite.suite()) + .collect::>(); + let ring = rustls::crypto::ring::default_provider() + .cipher_suites + .iter() + .map(|suite| suite.suite()) + .collect::>(); + assert_eq!(installed, ring); +} diff --git a/tests/unit/livekit.rs b/tests/unit/livekit.rs index cd610c63..bbfbe311 100644 --- a/tests/unit/livekit.rs +++ b/tests/unit/livekit.rs @@ -7633,3 +7633,78 @@ async fn thinking_release_rebases_existing_tool_continuation_waits() { ); server.await.unwrap(); } + +/// LiveKit's transport is vendored so its proxied `wss://` path runs on the +/// bundled roots; upstream refuses that path unless the machine's roots are +/// enabled. The connection runs in a child so `HTTPS_PROXY` reaches only that +/// process, and the fake proxy here sees the CONNECT and the TLS handshake +/// that follows it through the tunnel. +#[test] +fn livekit_signaling_tunnels_through_an_https_proxy() { + use std::io::{BufRead, Read, Write}; + + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let proxy = format!("http://{}", listener.local_addr().unwrap()); + + // Reported over a channel so a child that never reaches the proxy fails the + // test instead of leaving it blocked in accept. + let (seen, heard) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let (socket, _) = listener.accept().unwrap(); + let mut reader = std::io::BufReader::new(socket); + let mut request = String::new(); + reader.read_line(&mut request).unwrap(); + loop { + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + if line == "\r\n" { + break; + } + } + let mut socket = reader.into_inner(); + socket + .write_all(b"HTTP/1.1 200 Connection established\r\n\r\n") + .unwrap(); + let mut record = [0u8; 1]; + socket.read_exact(&mut record).unwrap(); + let _ = seen.send((request, record[0])); + }); + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "livekit::tests::livekit_proxy_child", + "--nocapture", + "--test-threads=1", + ]) + .env("HTTPS_PROXY", &proxy) + .env("CODETRIAL_LIVEKIT_PROXY_TEST_CHILD", "1") + .output() + .unwrap(); + assert!( + output.status.success(), + "child failed: {}", + String::from_utf8_lossy(&output.stdout) + ); + assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed")); + let (request, record) = heard + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("the child tunneled through the proxy"); + assert_eq!(request.trim_end(), "CONNECT livekit.example:443 HTTP/1.1"); + // A TLS handshake record: the ClientHello went into the tunnel. + assert_eq!(record, 0x16); +} + +#[tokio::test] +async fn livekit_proxy_child() { + if std::env::var_os("CODETRIAL_LIVEKIT_PROXY_TEST_CHILD").is_none() { + return; + } + crate::install_crypto_provider(); + let client = livekit_net::testing::native_ws_client(); + let error = client + .connect("wss://livekit.example/rtc".into(), Vec::new(), 5_000) + .await + .err() + .expect("the fake proxy ends the tunnel after the ClientHello"); + assert!(!error.to_string().contains("requires"), "{error}"); +} diff --git a/vendor/livekit-net/Cargo.toml b/vendor/livekit-net/Cargo.toml new file mode 100644 index 00000000..fd840a28 --- /dev/null +++ b/vendor/livekit-net/Cargo.toml @@ -0,0 +1,172 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "livekit-net" +version = "0.1.4" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Pluggable, host-providable network transport for LiveKit signalling" +readme = false +license = "Apache-2.0" +repository = "https://github.com/livekit/rust-sdks" + +[package.metadata.feature-combinations] +mode = "powerset" + +[features] +__rustls-tls = [ + "tokio-tungstenite?/__rustls-tls", + "reqwest?/__rustls", +] +default = [] +native = [ + "dep:tokio-tungstenite", + "dep:reqwest", + "dep:tokio", + "dep:futures-util", + "dep:base64", +] +native-tls = [ + "tokio-tungstenite?/native-tls", + "reqwest?/native-tls", +] +native-tls-vendored = [ + "tokio-tungstenite?/native-tls-vendored", + "reqwest?/native-tls-vendored", +] +rustls-tls-native-roots = [ + "tokio-tungstenite?/rustls-tls-native-roots", + "reqwest?/rustls-tls-native-roots", + "tokio-tungstenite?/__rustls-tls", + "dep:tokio-rustls", + "dep:rustls-native-certs", +] +rustls-tls-webpki-roots = [ + "tokio-tungstenite?/rustls-tls-webpki-roots", + "reqwest?/rustls-tls-webpki-roots", + "tokio-tungstenite?/__rustls-tls", + "dep:tokio-rustls", + "dep:webpki-roots", +] +uniffi = ["dep:uniffi"] + +[lib] +name = "livekit_net" +path = "src/lib.rs" + +[[test]] +name = "globals" +path = "tests/globals.rs" + +[[test]] +name = "native_parity" +path = "tests/native_parity.rs" + +[[test]] +name = "self_test" +path = "tests/self_test.rs" + +[dependencies.async-trait] +version = "0.1" + +[dependencies.base64] +version = "0.21" +features = ["std"] +optional = true + +[dependencies.futures-util] +version = "0.3" +features = ["sink"] +optional = true +default-features = false + +[dependencies.http] +version = "1.1" + +[dependencies.log] +version = "0.4" + +[dependencies.reqwest] +version = "0.12" +features = ["json"] +optional = true +default-features = false + +[dependencies.rustls-native-certs] +version = "0.8" +optional = true + +[dependencies.tokio] +version = "1" +features = [ + "sync", + "macros", + "io-util", + "net", + "time", + "rt-multi-thread", +] +optional = true +default-features = false + +[dependencies.tokio-rustls] +version = "0.26" +features = [ + "logging", + "tls12", + "ring", +] +optional = true +default-features = false + +[dependencies.tokio-tungstenite] +version = "0.30" +features = ["url"] +optional = true + +[dependencies.webpki-roots] +version = "1.0.10" +optional = true + +[dependencies.uniffi] +version = "0.31" +features = ["scaffolding-ffi-buffer-fns"] +optional = true + +[dependencies.url] +version = "2.3" + +[dev-dependencies.futures-util] +version = "0.3" +features = ["sink"] +default-features = false + +[dev-dependencies.tokio] +version = "1" +features = [ + "rt", + "rt-multi-thread", + "net", + "time", + "macros", + "io-util", +] +default-features = false + +[dev-dependencies.tokio-tungstenite] +version = "0.30" +features = ["url"] diff --git a/vendor/webrtc-sys-build/LICENSE b/vendor/livekit-net/LICENSE similarity index 100% rename from vendor/webrtc-sys-build/LICENSE rename to vendor/livekit-net/LICENSE diff --git a/vendor/livekit-net/README.md b/vendor/livekit-net/README.md new file mode 100644 index 00000000..0c46847a --- /dev/null +++ b/vendor/livekit-net/README.md @@ -0,0 +1,21 @@ +Vendored from [`livekit-net` 0.1.4](https://crates.io/crates/livekit-net), +upstream revision `fa46e8427050b6ee0c6e40670ff161ee3a2bbf5a`, `livekit-net/`. +Licensed under Apache-2.0; see LICENSE. + +Two changes from upstream: + +- The tokio-tungstenite dependency changes from 0.29 to 0.30, including its + upstream tests, and the unused direct bytes dependency is removed. The + companion tokio-tungstenite patch moves its bundled certificate roots to + webpki-roots 1, fixing RUSTSEC-2026-0381 without changing either transport + implementation. +- `src/native/proxy.rs` tunnels `wss://` through `HTTPS_PROXY` with the + `rustls-tls-webpki-roots` feature too, trusting the webpki-roots 1 roots the + direct path uses. Upstream refuses a proxied `wss://` connection unless + `rustls-tls-native-roots` is enabled, so LiveKit signaling failed on any + network that requires a proxy. `Cargo.toml` adds `tokio-rustls` and + `webpki-roots` to that feature for it. + +Remove the Cargo patch and this directory when upstream releases a network +transport using tokio-tungstenite 0.30 or later whose proxied `wss://` path +works with `rustls-tls-webpki-roots`. diff --git a/vendor/livekit-net/src/lib.rs b/vendor/livekit-net/src/lib.rs new file mode 100644 index 00000000..3a3e8d55 --- /dev/null +++ b/vendor/livekit-net/src/lib.rs @@ -0,0 +1,177 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod transport; +mod types; + +#[cfg(feature = "native")] +mod native; + +pub use transport::{ + HttpClient, HttpClientExt, HttpMethod, WsClient, WsConnectResult, WsConnection, +}; +pub use types::{Header, HttpResponse, TransportError}; + +use std::sync::{Arc, OnceLock}; + +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); + +/// Render a URL for logging with secrets stripped: userinfo (`user:password@`) +/// and the query string (which can carry an access token). Keeps scheme, host, +/// port, and path. +pub fn redact_url(url: &url::Url) -> String { + let mut u = url.clone(); + let _ = u.set_username(""); + let _ = u.set_password(None); + u.set_query(None); + u.set_fragment(None); + u.to_string() +} + +static WS: OnceLock> = OnceLock::new(); +static HTTP: OnceLock> = OnceLock::new(); + +/// Register the process-wide WebSocket client. Call once at startup, before the +/// first `connect`. A later call is ignored (first registration wins). +/// +/// Independent of [`set_http_client`]: a consumer that only needs HTTP (e.g. a +/// token source) can register that alone, and vice versa. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn set_ws_client(c: Arc) { + let _ = WS.set(c); +} + +/// Register the process-wide HTTP client. Call once at startup, before the first +/// request. A later call is ignored (first registration wins). +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn set_http_client(c: Arc) { + let _ = HTTP.set(c); +} + +/// Self-test: GET `url` via the registered HTTP client; returns the full response +/// (status + headers + body) so callers can assert the whole struct round-trips the FFI. +/// Errors if no client is registered or the transport fails. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub async fn self_test_http_get(url: String) -> Result { + let c = + http_client().ok_or_else(|| TransportError::Other("no http client registered".into()))?; + c.request(HttpMethod::Get, url, Vec::new(), None).await +} + +/// Self-test: connect, send `payload`, receive one frame, close; return the echoed bytes. +/// Errors if no client is registered, the transport fails, or the peer closes first. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub async fn self_test_ws_echo(url: String, payload: Vec) -> Result, TransportError> { + let c = ws_client().ok_or_else(|| TransportError::Other("no ws client registered".into()))?; + let conn = c.connect(url, Vec::new(), 5_000).await?.connection; + conn.send(payload).await?; + let got = conn.recv().await?.ok_or(TransportError::Closed)?; + conn.close().await; + Ok(got) +} + +/// Test probe: whether a host has explicitly registered an HTTP client via +/// [`set_http_client`]. +/// +/// Reports registration, not resolvability: on native builds [`http_client`] +/// still yields the built-in client when this returns `false`. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn has_http_client() -> bool { + HTTP.get().is_some() +} + +/// Test probe: whether a host has explicitly registered a WebSocket client via +/// [`set_ws_client`]. +/// +/// Reports registration, not resolvability: on native builds [`ws_client`] still +/// yields the built-in client when this returns `false`. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn has_ws_client() -> bool { + WS.get().is_some() +} + +/// Resolve the process-wide WebSocket client. +/// +/// Returns the explicitly registered client if any; otherwise, on native builds, +/// the built-in native client; otherwise `None`. +pub fn ws_client() -> Option> { + if let Some(c) = WS.get() { + return Some(Arc::clone(c)); + } + #[cfg(feature = "native")] + { + Some(native::native_ws_client()) + } + #[cfg(not(feature = "native"))] + { + None + } +} + +/// Resolve the process-wide HTTP client. +/// +/// Returns the explicitly registered client if any; otherwise, on native builds, +/// the built-in native client; otherwise `None`. +pub fn http_client() -> Option> { + if let Some(c) = HTTP.get() { + return Some(Arc::clone(c)); + } + #[cfg(feature = "native")] + { + Some(native::native_http_client()) + } + #[cfg(not(feature = "native"))] + { + None + } +} + +#[cfg(feature = "native")] +pub mod testing { + use crate::{HttpClient, WsClient}; + use std::sync::Arc; + /// A fresh native WebSocket client for tests (bypasses the global registry). + pub fn native_ws_client() -> Arc { + Arc::new(crate::native::NativeTransport::new()) + } + /// A fresh native HTTP client for tests (bypasses the global registry). + pub fn native_http_client() -> Arc { + Arc::new(crate::native::NativeTransport::new()) + } +} + +#[cfg(test)] +mod tests { + use super::redact_url; + + #[test] + fn redact_url_strips_proxy_credentials() { + let u = url::Url::parse("http://user:s3cret@proxy.example.com:8080/path").unwrap(); + let out = redact_url(&u); + assert_eq!(out, "http://proxy.example.com:8080/path"); + assert!(!out.contains("s3cret") && !out.contains("user")); + } + + #[test] + fn redact_url_strips_query_token() { + let u = url::Url::parse( + "wss://sfu.example.com/rtc/v1?access_token=abc.def.ghi&join_request=CAES", + ) + .unwrap(); + let out = redact_url(&u); + assert_eq!(out, "wss://sfu.example.com/rtc/v1"); + assert!(!out.contains("abc.def.ghi") && !out.contains("CAES")); + } +} diff --git a/vendor/livekit-net/src/native/connection.rs b/vendor/livekit-net/src/native/connection.rs new file mode 100644 index 00000000..60658863 --- /dev/null +++ b/vendor/livekit-net/src/native/connection.rs @@ -0,0 +1,86 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{TransportError, WsConnection}; +use futures_util::{ + stream::{SplitSink, SplitStream}, + SinkExt, StreamExt, +}; +use tokio::net::TcpStream; +use tokio::sync::Mutex; + +use tokio_tungstenite::{ + tungstenite::{error::ProtocolError, Error as WsError, Message}, + MaybeTlsStream, WebSocketStream, +}; + +type WebSocket = WebSocketStream>; + +pub struct NativeConnection { + writer: Mutex>, + reader: Mutex>, +} + +impl NativeConnection { + pub(super) fn new(ws: WebSocket) -> Self { + let (writer, reader) = ws.split(); + Self { writer: Mutex::new(writer), reader: Mutex::new(reader) } + } +} + +#[async_trait::async_trait] +impl WsConnection for NativeConnection { + async fn send(&self, frame: Vec) -> Result<(), TransportError> { + self.writer + .lock() + .await + .send(Message::Binary(frame.into())) + .await + .map_err(|e| TransportError::Connection(e.to_string())) + } + + async fn recv(&self) -> Result>, TransportError> { + let mut reader = self.reader.lock().await; + loop { + match reader.next().await { + Some(Ok(Message::Binary(data))) => return Ok(Some(data.to_vec())), + // Respond to WS ping internally; keep reading for the next app frame. + Some(Ok(Message::Ping(payload))) => { + let _ = self.writer.lock().await.send(Message::Pong(payload)).await; + } + Some(Ok(Message::Pong(_))) | Some(Ok(Message::Frame(_))) => continue, + Some(Ok(Message::Text(_))) => continue, // signalling never sends text + Some(Ok(Message::Close(_))) | None => return Ok(None), + Some(Err(WsError::Protocol(ProtocolError::ResetWithoutClosingHandshake))) => { + return Ok(None) + } + // TLS connection closed without close_notify - treat as normal close. + // Happens when the server closes the connection abruptly. + Some(Err(WsError::Io(ref io_err))) + if io_err.kind() == std::io::ErrorKind::UnexpectedEof => + { + return Ok(None) + } + Some(Err(WsError::ConnectionClosed)) | Some(Err(WsError::AlreadyClosed)) => { + return Ok(None) + } + Some(Err(e)) => return Err(TransportError::Connection(e.to_string())), + } + } + } + + async fn close(&self) { + let _ = self.writer.lock().await.close().await; + } +} diff --git a/vendor/livekit-net/src/native/mod.rs b/vendor/livekit-net/src/native/mod.rs new file mode 100644 index 00000000..6ca7ef78 --- /dev/null +++ b/vendor/livekit-net/src/native/mod.rs @@ -0,0 +1,141 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod connection; +mod proxy; + +use crate::{ + Header, HttpClient, HttpMethod, HttpResponse, TransportError, WsClient, WsConnectResult, +}; +use std::sync::Arc; + +fn error_chain(e: &dyn std::error::Error) -> String { + let mut msg = e.to_string(); + let mut src = e.source(); + while let Some(s) = src { + msg.push_str(": "); + msg.push_str(&s.to_string()); + src = s.source(); + } + msg +} + +/// The built-in native transport. One type implements both [`WsClient`] and +/// [`HttpClient`]. On the tokio backend it owns the reqwest client, so the +/// connection pool lives and dies with the transport object. +pub struct NativeTransport { + // reqwest pools connections per client, so a per-request client would redo + // TCP/TLS setup (and root-store loading) on every call. + http: reqwest::Client, +} + +impl NativeTransport { + pub(crate) fn new() -> Self { + Self { http: reqwest::Client::new() } + } +} + +/// The shared default transport behind the registry fallback. Memoized because +/// consumers resolve a client per request: a fresh transport per resolution +/// would defeat its connection pooling. +fn default_transport() -> Arc { + use std::sync::OnceLock; + static DEFAULT: OnceLock> = OnceLock::new(); + Arc::clone(DEFAULT.get_or_init(|| Arc::new(NativeTransport::new()))) +} + +pub(crate) fn native_ws_client() -> Arc { + default_transport() +} + +pub(crate) fn native_http_client() -> Arc { + default_transport() +} + +#[async_trait::async_trait] +impl WsClient for NativeTransport { + async fn connect( + &self, + url: String, + headers: Vec
, + timeout_ms: u64, + ) -> Result { + let parsed = + url::Url::parse(&url).map_err(|e| TransportError::Connection(e.to_string()))?; + + use tokio_tungstenite::tungstenite::client::IntoClientRequest; + + let mut request = parsed + .clone() + .into_client_request() + .map_err(|e| TransportError::Connection(e.to_string()))?; + for h in &headers { + let name: http::header::HeaderName = + h.name.parse().map_err(|_| TransportError::Other("bad header name".into()))?; + let value = http::header::HeaderValue::from_str(&h.value) + .map_err(|_| TransportError::Other("bad header value".into()))?; + request.headers_mut().insert(name, value); + } + + let connect_fut = async { + let ws = self::proxy::connect_ws(request, &parsed).await?; + Ok::<_, TransportError>(ws) + }; + + let ws = tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), connect_fut) + .await + .map_err(|_| TransportError::Timeout)??; + + Ok(WsConnectResult { connection: Arc::new(self::connection::NativeConnection::new(ws)) }) + } +} + +#[async_trait::async_trait] +impl HttpClient for NativeTransport { + async fn request( + &self, + method: HttpMethod, + url: String, + headers: Vec
, + body: Option>, + ) -> Result { + { + let client = &self.http; + let mut req = match method { + HttpMethod::Get => client.get(&url), + HttpMethod::Post => client.post(&url), + }; + for h in &headers { + req = req.header(&h.name, &h.value); + } + if let Some(body) = body { + req = req.body(body); + } + let res = req.send().await.map_err(|e| TransportError::Connection(error_chain(&e)))?; + let status = res.status().as_u16(); + let resp_headers = res + .headers() + .iter() + .filter_map(|(n, v)| { + v.to_str() + .ok() + .map(|v| Header { name: n.as_str().to_string(), value: v.to_string() }) + }) + .collect(); + let body = + res.bytes().await.map_err(|e| TransportError::Other(e.to_string()))?.to_vec(); + Ok(HttpResponse { status, headers: resp_headers, body }) + } + } +} diff --git a/vendor/livekit-net/src/native/proxy.rs b/vendor/livekit-net/src/native/proxy.rs new file mode 100644 index 00000000..0c03ee56 --- /dev/null +++ b/vendor/livekit-net/src/native/proxy.rs @@ -0,0 +1,245 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::TransportError; + +use std::env; + +/// Map a tungstenite error from a WS handshake to a [`TransportError`]. +/// +/// When the server returns an HTTP error response during the upgrade (e.g. 404 +/// on the /rtc endpoint), tungstenite surfaces it as `Error::Http(response)`. +/// We extract the status code and return `TransportError::Http { status }` so +/// callers can distinguish HTTP error codes (403, 404) from network errors. +fn map_ws_err(e: tokio_tungstenite::tungstenite::Error) -> TransportError { + use tokio_tungstenite::tungstenite::Error; + match e { + Error::Http(resp) => TransportError::Http { status: resp.status().as_u16() }, + other => TransportError::Connection(other.to_string()), + } +} + +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream as TokioTcpStream, +}; + +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; + +use tokio::net::TcpStream; + +pub(super) async fn connect_ws( + request: http::Request<()>, + url: &url::Url, +) -> Result>, TransportError> { + // Check for HTTP_PROXY or HTTPS_PROXY environment variables + let proxy_env = if url.scheme() == "wss" { + env::var("HTTPS_PROXY").or_else(|_| env::var("https_proxy")) + } else { + env::var("HTTP_PROXY").or_else(|_| env::var("http_proxy")) + }; + + // Connect directly or through proxy + let ws_stream = if let Ok(proxy_url) = proxy_env { + if !proxy_url.is_empty() { + let proxy_url = url::Url::parse(&proxy_url) + .map_err(|e| TransportError::Connection(format!("Invalid proxy URL: {}", e)))?; + log::info!("Using proxy: {}", crate::redact_url(&proxy_url)); + + let host = url + .host_str() + .ok_or_else(|| TransportError::Connection("Target URL has no host".into()))?; + + let port = url.port_or_known_default().ok_or_else(|| { + TransportError::Connection( + "Target URL has no port and no default for scheme".into(), + ) + })?; + + let proxy_host = proxy_url + .host_str() + .ok_or_else(|| TransportError::Connection("Proxy URL has no host".into()))?; + + let proxy_port = proxy_url.port_or_known_default().unwrap_or(80); + let proxy_addr = format!("{}:{}", proxy_host, proxy_port); + + let mut proxy_stream = TokioTcpStream::connect(proxy_addr) + .await + .map_err(|e| TransportError::Connection(e.to_string()))?; + + let mut proxy_auth_header = None; + if let Some(password) = proxy_url.password() { + use base64::Engine as _; + let auth = format!("{}:{}", proxy_url.username(), password); + let auth = + format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(auth)); + proxy_auth_header = Some(auth); + } + + // Send CONNECT request + let target = format!("{}:{}", host, port); + let mut connect_req = format!("CONNECT {} HTTP/1.1\r\nHost: {}\r\n", target, target); + + // Add proxy authorization if needed + if let Some(auth) = proxy_auth_header { + connect_req.push_str(&format!("Proxy-Authorization: {}\r\n", auth)); + } + + // Finalize request + connect_req.push_str("\r\n"); + + log::debug!("Sending CONNECT request to proxy"); + proxy_stream + .write_all(connect_req.as_bytes()) + .await + .map_err(|e| TransportError::Connection(e.to_string()))?; + + // Read and parse response + let mut response = Vec::new(); + let mut buf = [0u8; 4096]; + let mut headers_complete = false; + + while !headers_complete { + let n = proxy_stream + .read(&mut buf) + .await + .map_err(|e| TransportError::Connection(e.to_string()))?; + if n == 0 { + return Err(TransportError::Connection( + "Proxy connection closed while reading response".into(), + )); + } + + response.extend_from_slice(&buf[..n]); + + // Check if we've received the end of headers (double CRLF) + if response.windows(4).any(|w| w == b"\r\n\r\n") { + headers_complete = true; + } + } + + // Parse status line + let response_str = String::from_utf8_lossy(&response); + let status_line = response_str + .lines() + .next() + .ok_or_else(|| TransportError::Connection("Invalid proxy response".into()))?; + + // Check status code + if !status_line.contains("200") { + return Err(TransportError::Connection(format!( + "Proxy connection failed: {}", + status_line + ))); + } + + log::debug!("Proxy connection established to {}", target); + + // Create MaybeTlsStream based on original URL scheme + let stream = if url.scheme() == "wss" { + // Proxy TLS support needs a rustls root store, native or bundled + #[cfg(any( + feature = "rustls-tls-native-roots", + feature = "rustls-tls-webpki-roots" + ))] + { + // For WSS, we need to establish TLS over the proxy connection + use std::sync::Arc; + use tokio_rustls::{ + rustls::{self, pki_types::ServerName}, + TlsConnector, + }; + + let mut root_store = rustls::RootCertStore::empty(); + + // Load native root certificates + #[cfg(feature = "rustls-tls-native-roots")] + { + let cert_result = rustls_native_certs::load_native_certs(); + if !cert_result.errors.is_empty() { + log::warn!( + "Native root CA certificate loading errors: {:?}", + cert_result.errors + ); + } + // Not fatal when bundled roots are also enabled + #[cfg(not(feature = "rustls-tls-webpki-roots"))] + if cert_result.certs.is_empty() { + return Err(TransportError::Connection(format!( + "Could not load any native root certificates: {:?}", + cert_result.errors + ))); + } + let total = cert_result.certs.len(); + let (added, ignored) = + root_store.add_parsable_certificates(cert_result.certs); + log::debug!( + "Added {added}/{total} native root certificates ({ignored} ignored)" + ); + } + + // Load bundled Mozilla root certificates, as the direct path does + #[cfg(feature = "rustls-tls-webpki-roots")] + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let tls_config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = ServerName::try_from(host.to_owned()).map_err(|_| { + TransportError::Connection(format!("Invalid DNS name: {}", host)) + })?; + + let connector = TlsConnector::from(Arc::new(tls_config)); + let tls_stream = + connector.connect(server_name, proxy_stream).await.map_err(|e| { + TransportError::Connection(format!("TLS connection error: {}", e)) + })?; + + MaybeTlsStream::Rustls(tls_stream) + } + + #[cfg(not(any( + feature = "rustls-tls-native-roots", + feature = "rustls-tls-webpki-roots" + )))] + { + // Without a rustls root store, don't support proxy for WSS + return Err(TransportError::Connection( + "WSS over proxy requires a rustls-tls-*-roots feature".into(), + )); + } + } else { + // For plain WS, just use the proxy stream directly + MaybeTlsStream::Plain(proxy_stream) + }; + + // Now perform WebSocket handshake over the established connection + let (ws_stream, _) = tokio_tungstenite::client_async_with_config(request, stream, None) + .await + .map_err(map_ws_err)?; + ws_stream + } else { + // Proxy var is empty, connect directly + let (ws_stream, _) = connect_async(request).await.map_err(map_ws_err)?; + ws_stream + } + } else { + // No proxy env var set, connect directly + let (ws_stream, _) = connect_async(request).await.map_err(map_ws_err)?; + ws_stream + }; + + Ok(ws_stream) +} diff --git a/vendor/livekit-net/src/transport.rs b/vendor/livekit-net/src/transport.rs new file mode 100644 index 00000000..91bbdef0 --- /dev/null +++ b/vendor/livekit-net/src/transport.rs @@ -0,0 +1,106 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::types::{Header, HttpResponse, TransportError}; +use std::sync::Arc; + +/// A single open WebSocket connection. Control frames (ping/pong, close handshake) +/// are the implementation's own responsibility; only binary application frames cross here. +#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))] +#[async_trait::async_trait] +pub trait WsConnection: Send + Sync + 'static { + /// Send one binary application frame. + async fn send(&self, frame: Vec) -> Result<(), TransportError>; + /// Receive the next binary frame. `Ok(None)` means the peer/transport closed. + async fn recv(&self) -> Result>, TransportError>; + /// Close the connection, sending a close frame if the transport supports it. + async fn close(&self); +} + +/// The connection opened by [`WsClient::connect`]. +/// +/// A record wrapper, not a bare `Arc`: uniffi 0.31 cannot +/// lift a trait object returned from an async `with_foreign` method. +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct WsConnectResult { + pub connection: Arc, +} + +/// A host- or Rust-provided WebSocket transport. Opens the LiveKit signalling +/// WebSocket; knows nothing about LiveKit/protobuf. +#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))] +#[async_trait::async_trait] +pub trait WsClient: Send + Sync { + /// Open a WebSocket. `url` is the full ws(s):// URL including query string. + /// `timeout_ms` bounds connection establishment. Milliseconds, not `Duration`: + /// a `Duration` parameter breaks uniffi-dart's foreign codegen. + async fn connect( + &self, + url: String, + headers: Vec
, + timeout_ms: u64, + ) -> Result; +} + +/// The HTTP method for an [`HttpClient::request`]. +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HttpMethod { + Get, + Post, +} + +/// A host- or Rust-provided HTTP transport. Performs the request/response HTTP +/// calls LiveKit needs (connection validation, region discovery, token endpoints); +/// knows nothing about LiveKit/protobuf. +/// +/// Implementors provide the single [`request`](HttpClient::request) primitive; +/// [`HttpClientExt`] layers `get`/`post` on top, so adding verbs never widens the +/// implementation (or, for foreign impls, the FFI) surface. +#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))] +#[async_trait::async_trait] +pub trait HttpClient: Send + Sync { + /// Perform one HTTP request, sending `body` if present. + async fn request( + &self, + method: HttpMethod, + url: String, + headers: Vec
, + body: Option>, + ) -> Result; +} + +/// `get`/`post` conveniences over [`HttpClient::request`], blanket-implemented for +/// every [`HttpClient`] (including `dyn HttpClient`). Not part of the `HttpClient` +/// vtable, so verbs added here never reach implementors. +#[async_trait::async_trait] +pub trait HttpClientExt: HttpClient { + /// Perform a GET. + async fn get(&self, url: String, headers: Vec
) -> Result { + self.request(HttpMethod::Get, url, headers, None).await + } + + /// Perform a POST with the given body. + async fn post( + &self, + url: String, + headers: Vec
, + body: Vec, + ) -> Result { + self.request(HttpMethod::Post, url, headers, Some(body)).await + } +} + +#[async_trait::async_trait] +impl HttpClientExt for T {} diff --git a/vendor/livekit-net/src/types.rs b/vendor/livekit-net/src/types.rs new file mode 100644 index 00000000..3dc744bc --- /dev/null +++ b/vendor/livekit-net/src/types.rs @@ -0,0 +1,58 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt; + +/// A single HTTP/WebSocket request header. +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug, Clone)] +pub struct Header { + pub name: String, + pub value: String, +} + +/// The result of an HTTP request performed by the transport. +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug, Clone)] +pub struct HttpResponse { + pub status: u16, + /// Response headers, in receipt order. Lets callers read e.g. `Cache-Control`. + pub headers: Vec
, + pub body: Vec, +} + +/// Errors a transport implementation may return. Mapped onto `SignalError` by the caller. +#[cfg_attr(feature = "uniffi", derive(uniffi::Error))] +#[derive(Debug, Clone)] +pub enum TransportError { + Timeout, + Connection(String), + Http { status: u16 }, + Closed, + Other(String), +} + +impl fmt::Display for TransportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TransportError::Timeout => write!(f, "transport timed out"), + TransportError::Connection(m) => write!(f, "transport connection error: {m}"), + TransportError::Http { status } => write!(f, "transport http error: status {status}"), + TransportError::Closed => write!(f, "transport closed"), + TransportError::Other(m) => write!(f, "transport error: {m}"), + } + } +} + +impl std::error::Error for TransportError {} diff --git a/vendor/livekit-net/tests/globals.rs b/vendor/livekit-net/tests/globals.rs new file mode 100644 index 00000000..e2204d44 --- /dev/null +++ b/vendor/livekit-net/tests/globals.rs @@ -0,0 +1,76 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use livekit_net::{ + http_client, set_http_client, set_ws_client, ws_client, Header, HttpClientExt, HttpMethod, + HttpResponse, TransportError, WsClient, WsConnectResult, WsConnection, +}; +use std::sync::Arc; + +struct StubConn; + +#[async_trait::async_trait] +impl WsConnection for StubConn { + async fn send(&self, _frame: Vec) -> Result<(), TransportError> { + Ok(()) + } + async fn recv(&self) -> Result>, TransportError> { + Ok(None) + } + async fn close(&self) {} +} + +struct StubWsClient; + +#[async_trait::async_trait] +impl WsClient for StubWsClient { + async fn connect( + &self, + _url: String, + _headers: Vec
, + _timeout_ms: u64, + ) -> Result { + Ok(WsConnectResult { connection: Arc::new(StubConn) }) + } +} + +struct StubHttpClient; + +#[async_trait::async_trait] +impl livekit_net::HttpClient for StubHttpClient { + async fn request( + &self, + _method: HttpMethod, + _url: String, + _headers: Vec
, + _body: Option>, + ) -> Result { + Ok(HttpResponse { status: 200, headers: vec![], body: b"ok".to_vec() }) + } +} + +#[tokio::test] +async fn registered_clients_are_returned() { + set_ws_client(Arc::new(StubWsClient)); + set_http_client(Arc::new(StubHttpClient)); + + let http = http_client().expect("http client should be registered"); + let res = http.get("http://x".into(), vec![]).await.unwrap(); + assert_eq!(res.body, b"ok"); + assert_eq!(res.status, 200); + + let ws = ws_client().expect("ws client should be registered"); + let conn = ws.connect("ws://x".into(), vec![], 1000).await.unwrap().connection; + assert!(conn.recv().await.unwrap().is_none()); +} diff --git a/vendor/livekit-net/tests/native_parity.rs b/vendor/livekit-net/tests/native_parity.rs new file mode 100644 index 00000000..4bf74c1a --- /dev/null +++ b/vendor/livekit-net/tests/native_parity.rs @@ -0,0 +1,108 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "native")] + +use livekit_net::{Header, HttpClientExt}; + +// A hand-rolled one-shot HTTP server avoids adding a web-framework dep. +async fn spawn_http_once(status_line: &'static str, body: &'static str) -> u16 { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + tokio::spawn(async move { + let (mut sock, _) = listener.accept().await.unwrap(); + let mut buf = [0u8; 1024]; + let _ = sock.read(&mut buf).await; + let resp = format!("{status_line}\r\nContent-Length: {}\r\n\r\n{body}", body.len()); + sock.write_all(resp.as_bytes()).await.unwrap(); + }); + port +} + +#[tokio::test] +async fn ws_send_recv_roundtrip() { + // Minimal echo WS server using tokio-tungstenite (dev-dep). + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap(); + use futures_util::{SinkExt, StreamExt}; + while let Some(Ok(msg)) = ws.next().await { + if msg.is_binary() { + ws.send(msg).await.unwrap(); + } + } + }); + + let t = livekit_net::testing::native_ws_client(); + let result = t.connect(format!("ws://127.0.0.1:{port}"), vec![], 5000).await.unwrap(); + let conn = result.connection; + conn.send(b"ping".to_vec()).await.unwrap(); + let echoed = conn.recv().await.unwrap(); + assert_eq!(echoed, Some(b"ping".to_vec())); + conn.close().await; +} + +#[tokio::test] +async fn http_get_returns_status_and_body() { + let port = spawn_http_once("HTTP/1.1 200 OK", "hello").await; + let t = livekit_net::testing::native_http_client(); + let res = t + .get( + format!("http://127.0.0.1:{port}/settings/regions"), + vec![Header { name: "Authorization".into(), value: "Bearer x".into() }], + ) + .await + .unwrap(); + assert_eq!(res.status, 200); + assert_eq!(res.body, b"hello"); +} + +#[tokio::test] +async fn ws_connect_404_yields_transport_http_error() { + // Spin up a one-shot TCP server that accepts the WS upgrade request and + // immediately responds HTTP 404, simulating the /rtc/v1 endpoint not existing. + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + tokio::spawn(async move { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let (mut sock, _) = listener.accept().await.unwrap(); + // Read the upgrade request (discard it) + let mut buf = [0u8; 4096]; + let _ = sock.read(&mut buf).await; + // Respond with a 404 + let resp = "HTTP/1.1 404 Not Found +Content-Length: 0 + +"; + sock.write_all(resp.as_bytes()).await.unwrap(); + }); + + let t = livekit_net::testing::native_ws_client(); + let result = t.connect(format!("ws://127.0.0.1:{port}"), vec![], 5000).await; + + match result { + Err(livekit_net::TransportError::Http { status }) => { + assert_eq!(status, 404, "expected HTTP 404, got {status}"); + } + Ok(_) => { + panic!("expected Err(TransportError::Http {{ status: 404 }}), but connect succeeded") + } + Err(other) => { + panic!("expected Err(TransportError::Http {{ status: 404 }}), got Err({:?})", other) + } + } +} diff --git a/vendor/livekit-net/tests/self_test.rs b/vendor/livekit-net/tests/self_test.rs new file mode 100644 index 00000000..497a0c15 --- /dev/null +++ b/vendor/livekit-net/tests/self_test.rs @@ -0,0 +1,78 @@ +// Copyright 2026 LiveKit, Inc. (Apache-2.0) +use livekit_net::{ + has_http_client, has_ws_client, self_test_http_get, self_test_ws_echo, set_http_client, + set_ws_client, Header, HttpMethod, HttpResponse, TransportError, WsClient, WsConnectResult, + WsConnection, +}; +use std::sync::{Arc, Mutex}; + +struct EchoConn { + buf: Mutex>>, +} + +#[async_trait::async_trait] +impl WsConnection for EchoConn { + async fn send(&self, frame: Vec) -> Result<(), TransportError> { + *self.buf.lock().unwrap() = Some(frame); + Ok(()) + } + async fn recv(&self) -> Result>, TransportError> { + Ok(self.buf.lock().unwrap().take()) + } + async fn close(&self) {} +} + +struct EchoWsClient; + +#[async_trait::async_trait] +impl WsClient for EchoWsClient { + async fn connect( + &self, + _url: String, + _headers: Vec
, + _timeout_ms: u64, + ) -> Result { + Ok(WsConnectResult { connection: Arc::new(EchoConn { buf: Mutex::new(None) }) }) + } +} + +struct CannedHttpClient; + +#[async_trait::async_trait] +impl livekit_net::HttpClient for CannedHttpClient { + async fn request( + &self, + _method: HttpMethod, + _url: String, + _headers: Vec
, + _body: Option>, + ) -> Result { + Ok(HttpResponse { + status: 201, + headers: vec![Header { name: "x-test".into(), value: "1".into() }], + body: b"hello".to_vec(), + }) + } +} + +#[tokio::test] +async fn self_tests_round_trip_through_registered_clients() { + // Own test binary ⇒ fresh OnceLock, so nothing is registered yet. The probes + // report registration only, so a native build's built-in fallback doesn't + // show up here. + assert!(!has_http_client()); + assert!(!has_ws_client()); + + set_http_client(Arc::new(CannedHttpClient)); + set_ws_client(Arc::new(EchoWsClient)); + assert!(has_http_client()); + assert!(has_ws_client()); + + let resp = self_test_http_get("http://example/x".into()).await.unwrap(); + assert_eq!(resp.status, 201); + assert_eq!(resp.body, b"hello"); + assert_eq!(resp.headers.len(), 1); + + let echoed = self_test_ws_echo("ws://example/x".into(), b"ping".to_vec()).await.unwrap(); + assert_eq!(echoed, b"ping"); +} diff --git a/vendor/livekit-net/uniffi.toml b/vendor/livekit-net/uniffi.toml new file mode 100644 index 00000000..ca5a2801 --- /dev/null +++ b/vendor/livekit-net/uniffi.toml @@ -0,0 +1,10 @@ +[bindings.kotlin] +# The Kotlin checksum test is off because it fails on ARM devices. For more info, see the similar +# comment in livekit-ffi/uniffi.toml. +omit_checksums = true + +# Kotlin only, so the Rust source and the Swift/Python/Node bindings keep the original name. Lives +# here rather than in livekit-ffi/uniffi.toml because uniffi applies a rename table only to +# items from the crate that owns it. +[bindings.kotlin.rename] +"WsConnection.close" = "close_connection" diff --git a/vendor/tokio-tungstenite/Cargo.toml b/vendor/tokio-tungstenite/Cargo.toml new file mode 100644 index 00000000..abb0c46e --- /dev/null +++ b/vendor/tokio-tungstenite/Cargo.toml @@ -0,0 +1,222 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +rust-version = "1.85" +name = "tokio-tungstenite" +version = "0.30.0" +authors = [ + "Daniel Abramov ", + "Alexey Galakhov ", +] +build = false +include = [ + "examples/**/*", + "src/**/*", + "LICENSE", + "README.md", + "CHANGELOG.md", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Tokio binding for Tungstenite, the Lightweight stream-based WebSocket implementation" +homepage = "https://github.com/snapview/tokio-tungstenite" +documentation = "https://docs.rs/tokio-tungstenite/0.30.0" +readme = "README.md" +keywords = [ + "websocket", + "io", + "web", +] +categories = [ + "web-programming::websocket", + "network-programming", + "asynchronous", + "concurrency", +] +license = "MIT" +repository = "https://github.com/snapview/tokio-tungstenite" + +[package.metadata.docs.rs] +all-features = true + +[features] +__rustls-tls = [ + "rustls", + "rustls-pki-types", + "tokio-rustls", + "stream", + "tungstenite/__rustls-tls", + "handshake", +] +connect = [ + "stream", + "tokio/net", + "handshake", +] +default = [ + "connect", + "handshake", +] +handshake = ["tungstenite/handshake"] +native-tls = [ + "native-tls-crate", + "tokio-native-tls", + "stream", + "tungstenite/native-tls", + "handshake", +] +native-tls-vendored = [ + "native-tls", + "native-tls-crate/vendored", + "tungstenite/native-tls-vendored", +] +rustls-tls-native-roots = [ + "__rustls-tls", + "rustls-native-certs", +] +rustls-tls-webpki-roots = [ + "__rustls-tls", + "webpki-roots", +] +stream = [] +url = ["tungstenite/url"] + +[lib] +name = "tokio_tungstenite" +path = "src/lib.rs" + +[[example]] +name = "autobahn-client" +path = "examples/autobahn-client.rs" +required-features = ["connect"] + +[[example]] +name = "autobahn-server" +path = "examples/autobahn-server.rs" +required-features = ["handshake"] + +[[example]] +name = "client" +path = "examples/client.rs" +required-features = ["connect"] + +[[example]] +name = "echo-server" +path = "examples/echo-server.rs" +required-features = ["handshake"] + +[[example]] +name = "interval-server" +path = "examples/interval-server.rs" +required-features = ["handshake"] + +[[example]] +name = "server" +path = "examples/server.rs" +required-features = ["handshake"] + +[[example]] +name = "server-custom-accept" +path = "examples/server-custom-accept.rs" +required-features = ["handshake"] + +[[example]] +name = "server-headers" +path = "examples/server-headers.rs" +required-features = ["handshake"] + +[dependencies.futures-util] +version = "0.3.28" +features = [ + "sink", + "std", +] +default-features = false + +[dependencies.log] +version = "0.4.17" + +[dependencies.native-tls-crate] +version = "0.2.11" +optional = true +package = "native-tls" + +[dependencies.rustls] +version = "0.23.0" +optional = true +default-features = false + +[dependencies.rustls-native-certs] +version = "0.8.0" +optional = true + +[dependencies.rustls-pki-types] +version = "1.0" +optional = true + +[dependencies.tokio] +version = "1.0.0" +features = ["io-util"] +default-features = false + +[dependencies.tokio-native-tls] +version = "0.3.1" +optional = true + +[dependencies.tokio-rustls] +version = "0.26.0" +optional = true +default-features = false + +[dependencies.tungstenite] +version = "0.30.0" +default-features = false + +[dependencies.webpki-roots] +version = "1.0.10" +optional = true + +[dev-dependencies.env_logger] +version = "0.10.0" + +[dev-dependencies.futures-channel] +version = "0.3.28" + +[dev-dependencies.http-body-util] +version = "0.1" + +[dev-dependencies.hyper] +version = "1.0" +features = [ + "http1", + "server", +] +default-features = false + +[dev-dependencies.hyper-util] +version = "0.1" +features = ["tokio"] + +[dev-dependencies.tokio] +version = "1.27.0" +features = [ + "io-std", + "macros", + "net", + "rt-multi-thread", + "time", +] +default-features = false diff --git a/vendor/tokio-tungstenite/LICENSE b/vendor/tokio-tungstenite/LICENSE new file mode 100644 index 00000000..4bf5242e --- /dev/null +++ b/vendor/tokio-tungstenite/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 Daniel Abramov +Copyright (c) 2017 Alexey Galakhov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/tokio-tungstenite/README.md b/vendor/tokio-tungstenite/README.md new file mode 100644 index 00000000..8aa0390c --- /dev/null +++ b/vendor/tokio-tungstenite/README.md @@ -0,0 +1,10 @@ +Vendored from [`tokio-tungstenite` 0.30.0](https://crates.io/crates/tokio-tungstenite), +upstream revision `4994a078037a18a9347b8cf49291a5031d63d9e0`. +Licensed under MIT; see LICENSE. The upstream README is README.upstream.md. + +Only the webpki-roots dependency changes: 0.26 to 1.0.10, fixing +RUSTSEC-2026-0381. The upstream Rust source is unchanged. Version 0.26.11 +re-exports the version 1 trust anchors, so this removes the compatibility +wrapper while retaining its API and the patched certificate set. +Remove the Cargo patch and this directory when upstream releases a transport +using webpki-roots 1.0.10 or later. diff --git a/vendor/tokio-tungstenite/README.upstream.md b/vendor/tokio-tungstenite/README.upstream.md new file mode 100644 index 00000000..2c36a042 --- /dev/null +++ b/vendor/tokio-tungstenite/README.upstream.md @@ -0,0 +1,44 @@ +# tokio-tungstenite + +Asynchronous WebSockets for Tokio stack. + +[![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![crates.io](https://img.shields.io/crates/v/tokio-tungstenite.svg?maxAge=2592000)](https://crates.io/crates/tokio-tungstenite) +[![Build Status](https://github.com/snapview/tokio-tungstenite/actions/workflows/ci.yml/badge.svg)](https://github.com/snapview/tokio-tungstenite/actions) + +[Documentation](https://docs.rs/tokio-tungstenite) + +## Usage + +Add this in your `Cargo.toml`: + +```toml +[dependencies] +tokio-tungstenite = "0.30" +``` + +Take a look at the `examples/` directory for client and server examples. You may also want to get familiar with +[Tokio](https://github.com/tokio-rs/tokio) if you don't have any experience with it. + +## What is tokio-tungstenite? + +This crate is based on [`tungstenite-rs`](https://github.com/snapview/tungstenite-rs) Rust WebSocket library and provides `Tokio` bindings and wrappers for it, so you +can use it with non-blocking/asynchronous `TcpStream`s from and couple it together with other crates from `Tokio` stack. + +## Features + +As with [`tungstenite-rs`](https://github.com/snapview/tungstenite-rs) TLS is supported on all platforms using [`native-tls`](https://github.com/sfackler/rust-native-tls) or [`rustls`](https://github.com/rustls/rustls) through feature flags: `native-tls`, `rustls-tls-native-roots` or `rustls-tls-webpki-roots` feature flags. Neither is enabled by default. See the `Cargo.toml` for more information. If you require support for secure WebSockets (`wss://`) enable one of them. + +Note, that if you're using `rustls` features with `tokio-tungstenite` [version `0.23.0`](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md#0230) or higher, +you [might observe a panic](https://github.com/snapview/tokio-tungstenite/issues/336) that is easy to fix (see the issue). Check [the discussion](https://github.com/snapview/tokio-tungstenite/issues/353#issuecomment-2455100010) +for more details. + +## Is it performant? + +In essence, `tokio-tungstenite` is a wrapper for `tungstenite`, so the performance is capped by the performance of `tungstenite`. `tungstenite` +has a decent performance (it has been used in production for real-time communication software, video conferencing, etc), but it's definitely +not the fastest WebSocket library in the world at the moment of writing this note. + +We are aware of changes that both `tungstenite` and `tokio-tungstenite` require in order to make it on-par with slightly more performant libraries like `fastwebsockets`. In the course of past years we have merged several performance improvements submitted by the awesome community of Rust users who helped to improve the library! The more recent versions of `tokio-tungstenite` (`> 0.26.2`) are more performant and should be more on-par with `fastwebsockets`. + +For a quick summary of the pending performance problems/improvements, see [the comment](https://github.com/snapview/tungstenite-rs/issues/352#issuecomment-1537488614). diff --git a/vendor/tokio-tungstenite/examples/README.md b/vendor/tokio-tungstenite/examples/README.md new file mode 100644 index 00000000..b0bb00a1 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/README.md @@ -0,0 +1,9 @@ +Examples + +- [autobahn-client.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/autobahn-client.rs) +- [autobahn-server.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/autobahn-server.rs) +- [client.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/client.rs) +- [echo-server.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/echo-server.rs) +- [server.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/server.rs) +- [server-headers.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/server-headers.rs) +- [interval-server.rs](https://github.com/snapview/tokio-tungstenite/blob/master/examples/interval-server.rs) diff --git a/vendor/tokio-tungstenite/examples/autobahn-client.rs b/vendor/tokio-tungstenite/examples/autobahn-client.rs new file mode 100644 index 00000000..86676b25 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/autobahn-client.rs @@ -0,0 +1,54 @@ +use futures_util::{SinkExt, StreamExt}; +use log::*; +use tokio_tungstenite::{ + connect_async, + tungstenite::{Error, Result}, +}; + +const AGENT: &str = "Tungstenite"; + +async fn get_case_count() -> Result { + let (mut socket, _) = connect_async("ws://localhost:9001/getCaseCount").await?; + let msg = socket.next().await.expect("Can't fetch case count")?; + socket.close(None).await?; + Ok(msg.to_text()?.parse::().expect("Can't parse case count")) +} + +async fn update_reports() -> Result<()> { + let (mut socket, _) = + connect_async(&format!("ws://localhost:9001/updateReports?agent={}", AGENT)).await?; + socket.close(None).await?; + Ok(()) +} + +async fn run_test(case: u32) -> Result<()> { + info!("Running test case {}", case); + let case_url = &format!("ws://localhost:9001/runCase?case={}&agent={}", case, AGENT); + let (mut ws_stream, _) = connect_async(case_url).await?; + while let Some(msg) = ws_stream.next().await { + let msg = msg?; + if msg.is_text() || msg.is_binary() { + ws_stream.send(msg).await?; + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + env_logger::init(); + + let total = get_case_count().await.expect("Error getting case count"); + + for case in 1..=total { + if let Err(e) = run_test(case).await { + match e { + Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8(_) => (), + err => error!("Testcase failed: {}", err), + } + } + } + + update_reports().await.expect("Error updating reports"); +} diff --git a/vendor/tokio-tungstenite/examples/autobahn-server.rs b/vendor/tokio-tungstenite/examples/autobahn-server.rs new file mode 100644 index 00000000..0542b0dc --- /dev/null +++ b/vendor/tokio-tungstenite/examples/autobahn-server.rs @@ -0,0 +1,48 @@ +use futures_util::{SinkExt, StreamExt}; +use log::*; +use std::net::SocketAddr; +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::{ + accept_async, + tungstenite::{Error, Result}, +}; + +async fn accept_connection(peer: SocketAddr, stream: TcpStream) { + if let Err(e) = handle_connection(peer, stream).await { + match e { + Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8(_) => (), + err => error!("Error processing connection: {}", err), + } + } +} + +async fn handle_connection(peer: SocketAddr, stream: TcpStream) -> Result<()> { + let mut ws_stream = accept_async(stream).await.expect("Failed to accept"); + + info!("New WebSocket connection: {}", peer); + + while let Some(msg) = ws_stream.next().await { + let msg = msg?; + if msg.is_text() || msg.is_binary() { + ws_stream.send(msg).await?; + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + env_logger::init(); + + let addr = "127.0.0.1:9002"; + let listener = TcpListener::bind(&addr).await.expect("Can't listen"); + info!("Listening on: {}", addr); + + while let Ok((stream, _)) = listener.accept().await { + let peer = stream.peer_addr().expect("connected streams should have a peer address"); + info!("Peer address: {}", peer); + + tokio::spawn(accept_connection(peer, stream)); + } +} diff --git a/vendor/tokio-tungstenite/examples/client.rs b/vendor/tokio-tungstenite/examples/client.rs new file mode 100644 index 00000000..05a73757 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/client.rs @@ -0,0 +1,57 @@ +//! A simple example of hooking up stdin/stdout to a WebSocket stream. +//! +//! This example will connect to a server specified in the argument list and +//! then forward all data read on stdin to the server, printing out all data +//! received on stdout. +//! +//! Note that this is not currently optimized for performance, especially around +//! buffer management. Rather it's intended to show an example of working with a +//! client. +//! +//! You can use this example together with the `server` example. + +use std::env; + +use futures_util::{future, pin_mut, StreamExt}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; + +#[tokio::main] +async fn main() { + let url = + env::args().nth(1).unwrap_or_else(|| panic!("this program requires at least one argument")); + + let (stdin_tx, stdin_rx) = futures_channel::mpsc::unbounded(); + tokio::spawn(read_stdin(stdin_tx)); + + let (ws_stream, _) = connect_async(&url).await.expect("Failed to connect"); + println!("WebSocket handshake has been successfully completed"); + + let (write, read) = ws_stream.split(); + + let stdin_to_ws = stdin_rx.map(Ok).forward(write); + let ws_to_stdout = { + read.for_each(|message| async { + let data = message.unwrap().into_data(); + tokio::io::stdout().write_all(&data).await.unwrap(); + }) + }; + + pin_mut!(stdin_to_ws, ws_to_stdout); + future::select(stdin_to_ws, ws_to_stdout).await; +} + +// Our helper method which will read data from stdin and send it along the +// sender provided. +async fn read_stdin(tx: futures_channel::mpsc::UnboundedSender) { + let mut stdin = tokio::io::stdin(); + loop { + let mut buf = vec![0; 1024]; + let n = match stdin.read(&mut buf).await { + Err(_) | Ok(0) => break, + Ok(n) => n, + }; + buf.truncate(n); + tx.unbounded_send(Message::binary(buf)).unwrap(); + } +} diff --git a/vendor/tokio-tungstenite/examples/echo-server.rs b/vendor/tokio-tungstenite/examples/echo-server.rs new file mode 100644 index 00000000..603b0f10 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/echo-server.rs @@ -0,0 +1,53 @@ +//! A simple echo server. +//! +//! You can test this out by running: +//! +//! cargo run --example echo-server 127.0.0.1:12345 +//! +//! And then in another window run: +//! +//! cargo run --example client ws://127.0.0.1:12345/ +//! +//! Type a message into the client window, press enter to send it and +//! see it echoed back. + +use std::{env, io::Error}; + +use futures_util::{future, StreamExt, TryStreamExt}; +use log::info; +use tokio::net::{TcpListener, TcpStream}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let _ = env_logger::try_init(); + let addr = env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8080".to_string()); + + // Create the event loop and TCP listener we'll accept connections on. + let try_socket = TcpListener::bind(&addr).await; + let listener = try_socket.expect("Failed to bind"); + info!("Listening on: {}", addr); + + while let Ok((stream, _)) = listener.accept().await { + tokio::spawn(accept_connection(stream)); + } + + Ok(()) +} + +async fn accept_connection(stream: TcpStream) { + let addr = stream.peer_addr().expect("connected streams should have a peer address"); + info!("Peer address: {}", addr); + + let ws_stream = tokio_tungstenite::accept_async(stream) + .await + .expect("Error during the websocket handshake occurred"); + + info!("New WebSocket connection: {}", addr); + + let (write, read) = ws_stream.split(); + // We should not forward messages other than text or binary. + read.try_filter(|msg| future::ready(msg.is_text() || msg.is_binary())) + .forward(write) + .await + .expect("Failed to forward messages") +} diff --git a/vendor/tokio-tungstenite/examples/interval-server.rs b/vendor/tokio-tungstenite/examples/interval-server.rs new file mode 100644 index 00000000..4371c828 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/interval-server.rs @@ -0,0 +1,65 @@ +use futures_util::{SinkExt, StreamExt}; +use log::*; +use std::{net::SocketAddr, time::Duration}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::{ + accept_async, + tungstenite::{Error, Message, Result}, +}; + +async fn accept_connection(peer: SocketAddr, stream: TcpStream) { + if let Err(e) = handle_connection(peer, stream).await { + match e { + Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8(_) => (), + err => error!("Error processing connection: {}", err), + } + } +} + +async fn handle_connection(peer: SocketAddr, stream: TcpStream) -> Result<()> { + let ws_stream = accept_async(stream).await.expect("Failed to accept"); + info!("New WebSocket connection: {}", peer); + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + let mut interval = tokio::time::interval(Duration::from_millis(1000)); + + // Echo incoming WebSocket messages and send a message periodically every second. + + loop { + tokio::select! { + msg = ws_receiver.next() => { + match msg { + Some(msg) => { + let msg = msg?; + if msg.is_text() ||msg.is_binary() { + ws_sender.send(msg).await?; + } else if msg.is_close() { + break; + } + } + None => break, + } + } + _ = interval.tick() => { + ws_sender.send(Message::text("tick")).await?; + } + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + env_logger::init(); + + let addr = "127.0.0.1:9002"; + let listener = TcpListener::bind(&addr).await.expect("Can't listen"); + info!("Listening on: {}", addr); + + while let Ok((stream, _)) = listener.accept().await { + let peer = stream.peer_addr().expect("connected streams should have a peer address"); + info!("Peer address: {}", peer); + + tokio::spawn(accept_connection(peer, stream)); + } +} diff --git a/vendor/tokio-tungstenite/examples/server-custom-accept.rs b/vendor/tokio-tungstenite/examples/server-custom-accept.rs new file mode 100644 index 00000000..5880871c --- /dev/null +++ b/vendor/tokio-tungstenite/examples/server-custom-accept.rs @@ -0,0 +1,183 @@ +//! A chat server that broadcasts a message to all connections. +//! +//! This is a simple line-based server which accepts WebSocket connections, +//! reads lines from those connections, and broadcasts the lines to all other +//! connected clients. +//! +//! You can test this out by running: +//! +//! cargo run --example server 127.0.0.1:12345 +//! +//! And then in another window run: +//! +//! cargo run --example client ws://127.0.0.1:12345/socket +//! +//! You can run the second command in multiple windows and then chat between the +//! two, seeing the messages from the other client as they're received. For all +//! connected clients they'll all join the same room and see everyone else's +//! messages. + +use std::{ + collections::HashMap, + convert::Infallible, + env, + net::SocketAddr, + sync::{Arc, Mutex}, +}; + +use hyper::{ + body::Incoming, + header::{ + HeaderValue, CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, + UPGRADE, + }, + server::conn::http1, + service::service_fn, + upgrade::Upgraded, + Method, Request, Response, StatusCode, Version, +}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + +use futures_channel::mpsc::{unbounded, UnboundedSender}; +use futures_util::{future, pin_mut, stream::TryStreamExt, StreamExt}; + +use tokio_tungstenite::{ + tungstenite::{ + handshake::derive_accept_key, + protocol::{Message, Role}, + }, + WebSocketStream, +}; + +type Tx = UnboundedSender; +type PeerMap = Arc>>; +type Body = http_body_util::Full; + +async fn handle_connection( + peer_map: PeerMap, + ws_stream: WebSocketStream>, + addr: SocketAddr, +) { + println!("WebSocket connection established: {}", addr); + + // Insert the write part of this peer to the peer map. + let (tx, rx) = unbounded(); + peer_map.lock().unwrap().insert(addr, tx); + + let (outgoing, incoming) = ws_stream.split(); + + let broadcast_incoming = incoming.try_for_each(|msg| { + println!("Received a message from {}: {}", addr, msg.to_text().unwrap()); + let peers = peer_map.lock().unwrap(); + + // We want to broadcast the message to everyone except ourselves. + let broadcast_recipients = + peers.iter().filter(|(peer_addr, _)| peer_addr != &&addr).map(|(_, ws_sink)| ws_sink); + + for recp in broadcast_recipients { + recp.unbounded_send(msg.clone()).unwrap(); + } + + future::ok(()) + }); + + let receive_from_others = rx.map(Ok).forward(outgoing); + + pin_mut!(broadcast_incoming, receive_from_others); + future::select(broadcast_incoming, receive_from_others).await; + + println!("{} disconnected", &addr); + peer_map.lock().unwrap().remove(&addr); +} + +async fn handle_request( + peer_map: PeerMap, + mut req: Request, + addr: SocketAddr, +) -> Result, Infallible> { + println!("Received a new, potentially ws handshake"); + println!("The request's path is: {}", req.uri().path()); + println!("The request's headers are:"); + for (ref header, _value) in req.headers() { + println!("* {}", header); + } + let upgrade = HeaderValue::from_static("Upgrade"); + let websocket = HeaderValue::from_static("websocket"); + let headers = req.headers(); + let key = headers.get(SEC_WEBSOCKET_KEY); + let derived = key.map(|k| derive_accept_key(k.as_bytes())); + if req.method() != Method::GET + || req.version() < Version::HTTP_11 + || !headers + .get(CONNECTION) + .and_then(|h| h.to_str().ok()) + .map(|h| { + h.split(|c| c == ' ' || c == ',') + .any(|p| p.eq_ignore_ascii_case(upgrade.to_str().unwrap())) + }) + .unwrap_or(false) + || !headers + .get(UPGRADE) + .and_then(|h| h.to_str().ok()) + .map(|h| h.eq_ignore_ascii_case("websocket")) + .unwrap_or(false) + || !headers.get(SEC_WEBSOCKET_VERSION).map(|h| h == "13").unwrap_or(false) + || key.is_none() + || req.uri() != "/socket" + { + return Ok(Response::new(Body::from("Hello World!"))); + } + let ver = req.version(); + tokio::task::spawn(async move { + match hyper::upgrade::on(&mut req).await { + Ok(upgraded) => { + let upgraded = TokioIo::new(upgraded); + handle_connection( + peer_map, + WebSocketStream::from_raw_socket(upgraded, Role::Server, None).await, + addr, + ) + .await; + } + Err(e) => println!("upgrade error: {}", e), + } + }); + let mut res = Response::new(Body::default()); + *res.status_mut() = StatusCode::SWITCHING_PROTOCOLS; + *res.version_mut() = ver; + res.headers_mut().append(CONNECTION, upgrade); + res.headers_mut().append(UPGRADE, websocket); + res.headers_mut().append(SEC_WEBSOCKET_ACCEPT, derived.unwrap().parse().unwrap()); + // Let's add an additional header to our response to the client. + res.headers_mut().append("MyCustomHeader", ":)".parse().unwrap()); + res.headers_mut().append("SOME_TUNGSTENITE_HEADER", "header_value".parse().unwrap()); + Ok(res) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let state = PeerMap::new(Mutex::new(HashMap::new())); + + let addr = + env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8080".to_string()).parse::()?; + + let listener = TcpListener::bind(addr).await?; + + loop { + let (stream, remote_addr) = listener.accept().await?; + let state = state.clone(); + + tokio::spawn(async move { + let io = TokioIo::new(stream); + + let service = service_fn(move |req| handle_request(state.clone(), req, remote_addr)); + + let conn = http1::Builder::new().serve_connection(io, service).with_upgrades(); + + if let Err(err) = conn.await { + eprintln!("failed to serve connection: {err:?}"); + } + }); + } +} diff --git a/vendor/tokio-tungstenite/examples/server-headers.rs b/vendor/tokio-tungstenite/examples/server-headers.rs new file mode 100644 index 00000000..f8b729f6 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/server-headers.rs @@ -0,0 +1,84 @@ +//! Read/Write headers on server example +//! +//! Run with logs: +//! Linux: +//! ```sh +//! RUST_LOG=debug cargo run --example server-headers +//! ``` +//! Windows +//! ```sh +//! cmd /c "set RUST_LOG=debug && cargo run --example server-headers" +//! ``` +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::{ + accept_hdr_async, + tungstenite::{ + connect, + handshake::server::{Request, Response}, + Message, + }, +}; +#[macro_use] +extern crate log; +use futures_util::{SinkExt, StreamExt}; + +#[tokio::main] +async fn main() { + env_logger::builder().format_timestamp(None).init(); + + tokio::spawn(async move { + server().await; + }); + client(); +} + +async fn server() { + let server = TcpListener::bind("127.0.0.1:8080").await.unwrap(); + + while let Ok((stream, _)) = server.accept().await { + tokio::spawn(accept_connection(stream)); + } +} + +async fn accept_connection(stream: TcpStream) { + let callback = |req: &Request, mut response: Response| { + debug!("Received a new ws handshake"); + debug!("The request's path is: {}", req.uri().path()); + debug!("The request's headers are:"); + for (ref header, _value) in req.headers() { + debug!("* {}: {:?}", header, _value); + } + + let headers = response.headers_mut(); + headers.append("MyCustomHeader", ":)".parse().unwrap()); + + Ok(response) + }; + let mut ws_stream = accept_hdr_async(stream, callback) + .await + .expect("Error during the websocket handshake occurred"); + + while let Some(msg) = ws_stream.next().await { + let msg = msg.unwrap(); + if msg.is_text() || msg.is_binary() { + debug!("Server on message: {:?}", &msg); + ws_stream.send(msg).await.unwrap(); + } + } +} + +fn client() { + let (mut socket, response) = connect("ws://localhost:8080/socket").expect("Can't connect"); + debug!("Connected to the server"); + debug!("Response HTTP code: {}", response.status()); + debug!("Response contains the following headers:"); + for (ref header, _value) in response.headers() { + debug!("* {}: {:?}", header, _value); + } + + socket.send(Message::Text("Hello WebSocket".into())).unwrap(); + loop { + let msg = socket.read().expect("Error reading message"); + debug!("Received: {}", msg); + } +} diff --git a/vendor/tokio-tungstenite/examples/server.rs b/vendor/tokio-tungstenite/examples/server.rs new file mode 100644 index 00000000..288aeaa5 --- /dev/null +++ b/vendor/tokio-tungstenite/examples/server.rs @@ -0,0 +1,92 @@ +//! A chat server that broadcasts a message to all connections. +//! +//! This is a simple line-based server which accepts WebSocket connections, +//! reads lines from those connections, and broadcasts the lines to all other +//! connected clients. +//! +//! You can test this out by running: +//! +//! cargo run --example server 127.0.0.1:12345 +//! +//! And then in another window run: +//! +//! cargo run --example client ws://127.0.0.1:12345/ +//! +//! You can run the second command in multiple windows and then chat between the +//! two, seeing the messages from the other client as they're received. For all +//! connected clients they'll all join the same room and see everyone else's +//! messages. + +use std::{ + collections::HashMap, + env, + io::Error as IoError, + net::SocketAddr, + sync::{Arc, Mutex}, +}; + +use futures_channel::mpsc::{unbounded, UnboundedSender}; +use futures_util::{future, pin_mut, stream::TryStreamExt, StreamExt}; + +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::tungstenite::protocol::Message; + +type Tx = UnboundedSender; +type PeerMap = Arc>>; + +async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr) { + println!("Incoming TCP connection from: {}", addr); + + let ws_stream = tokio_tungstenite::accept_async(raw_stream) + .await + .expect("Error during the websocket handshake occurred"); + println!("WebSocket connection established: {}", addr); + + // Insert the write part of this peer to the peer map. + let (tx, rx) = unbounded(); + peer_map.lock().unwrap().insert(addr, tx); + + let (outgoing, incoming) = ws_stream.split(); + + let broadcast_incoming = incoming.try_for_each(|msg| { + println!("Received a message from {}: {}", addr, msg.to_text().unwrap()); + let peers = peer_map.lock().unwrap(); + + // We want to broadcast the message to everyone except ourselves. + let broadcast_recipients = + peers.iter().filter(|(peer_addr, _)| peer_addr != &&addr).map(|(_, ws_sink)| ws_sink); + + for recp in broadcast_recipients { + recp.unbounded_send(msg.clone()).unwrap(); + } + + future::ok(()) + }); + + let receive_from_others = rx.map(Ok).forward(outgoing); + + pin_mut!(broadcast_incoming, receive_from_others); + future::select(broadcast_incoming, receive_from_others).await; + + println!("{} disconnected", &addr); + peer_map.lock().unwrap().remove(&addr); +} + +#[tokio::main] +async fn main() -> Result<(), IoError> { + let addr = env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8080".to_string()); + + let state = PeerMap::new(Mutex::new(HashMap::new())); + + // Create the event loop and TCP listener we'll accept connections on. + let try_socket = TcpListener::bind(&addr).await; + let listener = try_socket.expect("Failed to bind"); + println!("Listening on: {}", addr); + + // Let's spawn the handling of each connection in a separate task. + while let Ok((stream, addr)) = listener.accept().await { + tokio::spawn(handle_connection(state.clone(), stream, addr)); + } + + Ok(()) +} diff --git a/vendor/tokio-tungstenite/src/compat.rs b/vendor/tokio-tungstenite/src/compat.rs new file mode 100644 index 00000000..17fbd1f9 --- /dev/null +++ b/vendor/tokio-tungstenite/src/compat.rs @@ -0,0 +1,203 @@ +use log::*; +use std::{ + io::{Read, Write}, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_util::task; +use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tungstenite::Error as WsError; + +pub(crate) enum ContextWaker { + Read, + Write, +} + +#[derive(Debug)] +pub(crate) struct AllowStd { + inner: S, + // We have the problem that external read operations (i.e. the Stream impl) + // can trigger both read (AsyncRead) and write (AsyncWrite) operations on + // the underyling stream. At the same time write operations (i.e. the Sink + // impl) can trigger write operations (AsyncWrite) too. + // Both the Stream and the Sink can be used on two different tasks, but it + // is required that AsyncRead and AsyncWrite are only ever used by a single + // task (or better: with a single waker) at a time. + // + // Doing otherwise would cause only the latest waker to be remembered, so + // in our case either the Stream or the Sink impl would potentially wait + // forever to be woken up because only the other one would've been woken + // up. + // + // To solve this we implement a waker proxy that has two slots (one for + // read, one for write) to store wakers. One waker proxy is always passed + // to the AsyncRead, the other to AsyncWrite so that they will only ever + // have to store a single waker, but internally we dispatch any wakeups to + // up to two actual wakers (one from the Sink impl and one from the Stream + // impl). + // + // write_waker_proxy is always used for AsyncWrite, read_waker_proxy for + // AsyncRead. The read_waker slots of both are used for the Stream impl + // (and handshaking), the write_waker slots for the Sink impl. + write_waker_proxy: Arc, + read_waker_proxy: Arc, +} + +// Internal trait used only in the Handshake module for registering +// the waker for the context used during handshaking. We're using the +// read waker slot for this, but any would do. +// +// Don't ever use this from multiple tasks at the same time! +pub(crate) trait SetWaker { + fn set_waker(&self, waker: &task::Waker); +} + +impl SetWaker for AllowStd { + fn set_waker(&self, waker: &task::Waker) { + self.set_waker(ContextWaker::Read, waker); + } +} + +impl AllowStd { + pub(crate) fn new(inner: S, waker: &task::Waker) -> Self { + let res = Self { + inner, + write_waker_proxy: Default::default(), + read_waker_proxy: Default::default(), + }; + + // Register the handshake waker as read waker for both proxies, + // see also the SetWaker trait. + res.write_waker_proxy.read_waker.register(waker); + res.read_waker_proxy.read_waker.register(waker); + + res + } + + // Set the read or write waker for our proxies. + // + // Read: this is only supposed to be called by read (or handshake) operations, i.e. the Stream + // impl on the WebSocketStream. + // Reading can also cause writes to happen, e.g. in case of Message::Ping handling. + // + // Write: this is only supposde to be called by write operations, i.e. the Sink impl on the + // WebSocketStream. + pub(crate) fn set_waker(&self, kind: ContextWaker, waker: &task::Waker) { + match kind { + ContextWaker::Read => { + self.write_waker_proxy.read_waker.register(waker); + self.read_waker_proxy.read_waker.register(waker); + } + ContextWaker::Write => { + self.write_waker_proxy.write_waker.register(waker); + self.read_waker_proxy.write_waker.register(waker); + } + } + } + + /// Returns the underlying stream. + pub(crate) fn into_inner(self) -> S { + self.inner + } +} + +// Proxy Waker that we pass to the internal AsyncRead/Write of the +// stream underlying the websocket. We have two slots here for the +// actual wakers to allow external read operations to trigger both +// reads and writes, and the same for writes. +#[derive(Debug, Default)] +struct WakerProxy { + read_waker: task::AtomicWaker, + write_waker: task::AtomicWaker, +} + +impl task::ArcWake for WakerProxy { + fn wake_by_ref(arc_self: &Arc) { + arc_self.read_waker.wake(); + arc_self.write_waker.wake(); + } +} + +impl AllowStd +where + S: Unpin, +{ + fn with_context(&mut self, kind: ContextWaker, f: F) -> Poll> + where + F: FnOnce(&mut Context<'_>, Pin<&mut S>) -> Poll>, + { + trace!("{}:{} AllowStd.with_context", file!(), line!()); + let waker = match kind { + ContextWaker::Read => task::waker_ref(&self.read_waker_proxy), + ContextWaker::Write => task::waker_ref(&self.write_waker_proxy), + }; + let mut context = task::Context::from_waker(&waker); + f(&mut context, Pin::new(&mut self.inner)) + } + + pub(crate) fn get_mut(&mut self) -> &mut S { + &mut self.inner + } + + pub(crate) fn get_ref(&self) -> &S { + &self.inner + } +} + +impl Read for AllowStd +where + S: AsyncRead + Unpin, +{ + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + trace!("{}:{} Read.read", file!(), line!()); + let mut buf = ReadBuf::new(buf); + match self.with_context(ContextWaker::Read, |ctx, stream| { + trace!("{}:{} Read.with_context read -> poll_read", file!(), line!()); + stream.poll_read(ctx, &mut buf) + }) { + Poll::Ready(Ok(_)) => Ok(buf.filled().len()), + Poll::Ready(Err(err)) => Err(err), + Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), + } + } +} + +impl Write for AllowStd +where + S: AsyncWrite + Unpin, +{ + fn write(&mut self, buf: &[u8]) -> std::io::Result { + trace!("{}:{} Write.write", file!(), line!()); + match self.with_context(ContextWaker::Write, |ctx, stream| { + trace!("{}:{} Write.with_context write -> poll_write", file!(), line!()); + stream.poll_write(ctx, buf) + }) { + Poll::Ready(r) => r, + Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + trace!("{}:{} Write.flush", file!(), line!()); + match self.with_context(ContextWaker::Write, |ctx, stream| { + trace!("{}:{} Write.with_context flush -> poll_flush", file!(), line!()); + stream.poll_flush(ctx) + }) { + Poll::Ready(r) => r, + Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), + } + } +} + +pub(crate) fn cvt(r: Result) -> Poll> { + match r { + Ok(v) => Poll::Ready(Ok(v)), + Err(WsError::Io(ref e)) if e.kind() == std::io::ErrorKind::WouldBlock => { + trace!("WouldBlock"); + Poll::Pending + } + Err(e) => Poll::Ready(Err(e)), + } +} diff --git a/vendor/tokio-tungstenite/src/connect.rs b/vendor/tokio-tungstenite/src/connect.rs new file mode 100644 index 00000000..989349a9 --- /dev/null +++ b/vendor/tokio-tungstenite/src/connect.rs @@ -0,0 +1,98 @@ +//! Connection helper. +use tokio::net::TcpStream; + +use tungstenite::{ + error::{Error, UrlError}, + handshake::client::{Request, Response}, + protocol::WebSocketConfig, +}; + +use crate::{domain, stream::MaybeTlsStream, Connector, IntoClientRequest, WebSocketStream}; + +/// Connect to a given URL. +/// +/// Accepts any request that implements [`IntoClientRequest`], which is often just `&str`, but can +/// be a variety of types such as `httparse::Request` or [`tungstenite::http::Request`] for more +/// complex uses. +/// +/// ```no_run +/// # use tungstenite::client::IntoClientRequest; +/// +/// # async fn test() { +/// use tungstenite::http::{Method, Request}; +/// use tokio_tungstenite::connect_async; +/// +/// let mut request = "wss://api.example.com".into_client_request().unwrap(); +/// request.headers_mut().insert("api-key", "42".parse().unwrap()); +/// +/// let (stream, response) = connect_async(request).await.unwrap(); +/// # } +/// ``` +pub async fn connect_async( + request: R, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + connect_async_with_config(request, None, false).await +} + +/// The same as `connect_async()` but the one can specify a websocket configuration. +/// Please refer to `connect_async()` for more details. `disable_nagle` specifies if +/// the Nagle's algorithm must be disabled, i.e. `set_nodelay(true)`. If you don't know +/// what the Nagle's algorithm is, better leave it set to `false`. +pub async fn connect_async_with_config( + request: R, + config: Option, + disable_nagle: bool, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + connect(request.into_client_request()?, config, disable_nagle, None).await +} + +/// The same as `connect_async()` but the one can specify a websocket configuration, +/// and a TLS connector to use. Please refer to `connect_async()` for more details. +/// `disable_nagle` specifies if the Nagle's algorithm must be disabled, i.e. +/// `set_nodelay(true)`. If you don't know what the Nagle's algorithm is, better +/// leave it to `false`. +#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] +pub async fn connect_async_tls_with_config( + request: R, + config: Option, + disable_nagle: bool, + connector: Option, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + connect(request.into_client_request()?, config, disable_nagle, connector).await +} + +async fn connect( + request: Request, + config: Option, + disable_nagle: bool, + connector: Option, +) -> Result<(WebSocketStream>, Response), Error> { + let domain = domain(&request)?; + let port = request + .uri() + .port_u16() + .or_else(|| match request.uri().scheme_str() { + Some("wss") => Some(443), + Some("ws") => Some(80), + _ => None, + }) + .ok_or(Error::Url(UrlError::UnsupportedUrlScheme))?; + + let addr = format!("{domain}:{port}"); + let socket = TcpStream::connect(addr).await.map_err(Error::Io)?; + + if disable_nagle { + socket.set_nodelay(true)?; + } + + crate::tls::client_async_tls_with_config(request, socket, config, connector).await +} diff --git a/vendor/tokio-tungstenite/src/handshake.rs b/vendor/tokio-tungstenite/src/handshake.rs new file mode 100644 index 00000000..aa512769 --- /dev/null +++ b/vendor/tokio-tungstenite/src/handshake.rs @@ -0,0 +1,179 @@ +#[cfg(feature = "handshake")] +use crate::compat::SetWaker; +use crate::{compat::AllowStd, WebSocketStream}; +use log::*; +use std::{ + future::Future, + io::{Read, Write}, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tungstenite::WebSocket; +#[cfg(feature = "handshake")] +use tungstenite::{ + handshake::{ + client::Response, server::Callback, HandshakeError as Error, HandshakeRole, + MidHandshake as WsHandshake, + }, + ClientHandshake, ServerHandshake, +}; + +pub(crate) async fn without_handshake(stream: S, f: F) -> WebSocketStream +where + F: FnOnce(AllowStd) -> WebSocket> + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + let start = SkippedHandshakeFuture(Some(SkippedHandshakeFutureInner { f, stream })); + + let ws = start.await; + + WebSocketStream::new(ws) +} + +struct SkippedHandshakeFuture(Option>); +struct SkippedHandshakeFutureInner { + f: F, + stream: S, +} + +impl Future for SkippedHandshakeFuture +where + F: FnOnce(AllowStd) -> WebSocket> + Unpin, + S: Unpin, + AllowStd: Read + Write, +{ + type Output = WebSocket>; + + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let inner = self.get_mut().0.take().expect("future polled after completion"); + trace!("Setting context when skipping handshake"); + let stream = AllowStd::new(inner.stream, ctx.waker()); + + Poll::Ready((inner.f)(stream)) + } +} + +#[cfg(feature = "handshake")] +struct MidHandshake(Option>); + +#[cfg(feature = "handshake")] +enum StartedHandshake { + Done(Role::FinalResult), + Mid(WsHandshake), +} + +#[cfg(feature = "handshake")] +struct StartedHandshakeFuture(Option>); +#[cfg(feature = "handshake")] +struct StartedHandshakeFutureInner { + f: F, + stream: S, +} + +#[cfg(feature = "handshake")] +async fn handshake(stream: S, f: F) -> Result> +where + Role: HandshakeRole + Unpin, + Role::InternalStream: SetWaker + Unpin, + F: FnOnce(AllowStd) -> Result> + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + let start = StartedHandshakeFuture(Some(StartedHandshakeFutureInner { f, stream })); + + match start.await? { + StartedHandshake::Done(r) => Ok(r), + StartedHandshake::Mid(s) => { + let res: Result> = MidHandshake::(Some(s)).await; + res + } + } +} + +#[cfg(feature = "handshake")] +pub(crate) async fn client_handshake( + stream: S, + f: F, +) -> Result<(WebSocketStream, Response), Error>>> +where + F: FnOnce( + AllowStd, + ) -> Result< + > as HandshakeRole>::FinalResult, + Error>>, + > + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + let result = handshake(stream, f).await?; + let (s, r) = result; + Ok((WebSocketStream::new(s), r)) +} + +#[cfg(feature = "handshake")] +pub(crate) async fn server_handshake( + stream: S, + f: F, +) -> Result, Error, C>>> +where + C: Callback + Unpin, + F: FnOnce( + AllowStd, + ) -> Result< + , C> as HandshakeRole>::FinalResult, + Error, C>>, + > + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + let s: WebSocket> = handshake(stream, f).await?; + Ok(WebSocketStream::new(s)) +} + +#[cfg(feature = "handshake")] +impl Future for StartedHandshakeFuture +where + Role: HandshakeRole, + Role::InternalStream: SetWaker + Unpin, + F: FnOnce(AllowStd) -> Result> + Unpin, + S: Unpin, + AllowStd: Read + Write, +{ + type Output = Result, Error>; + + fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let inner = self.0.take().expect("future polled after completion"); + trace!("Setting ctx when starting handshake"); + let stream = AllowStd::new(inner.stream, ctx.waker()); + + match (inner.f)(stream) { + Ok(r) => Poll::Ready(Ok(StartedHandshake::Done(r))), + Err(Error::Interrupted(mid)) => Poll::Ready(Ok(StartedHandshake::Mid(mid))), + Err(Error::Failure(e)) => Poll::Ready(Err(Error::Failure(e))), + } + } +} + +#[cfg(feature = "handshake")] +impl Future for MidHandshake +where + Role: HandshakeRole + Unpin, + Role::InternalStream: SetWaker + Unpin, +{ + type Output = Result>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut s = self.as_mut().0.take().expect("future polled after completion"); + + let machine = s.get_mut(); + trace!("Setting context in handshake"); + machine.get_mut().set_waker(cx.waker()); + + match s.handshake() { + Ok(stream) => Poll::Ready(Ok(stream)), + Err(Error::Failure(e)) => Poll::Ready(Err(Error::Failure(e))), + Err(Error::Interrupted(mid)) => { + self.0 = Some(mid); + Poll::Pending + } + } + } +} diff --git a/vendor/tokio-tungstenite/src/lib.rs b/vendor/tokio-tungstenite/src/lib.rs new file mode 100644 index 00000000..3091627a --- /dev/null +++ b/vendor/tokio-tungstenite/src/lib.rs @@ -0,0 +1,457 @@ +//! Async WebSocket usage. +//! +//! This library is an implementation of WebSocket handshakes and streams. It +//! is based on the crate which implements all required WebSocket protocol +//! logic. So this crate basically just brings tokio support / tokio integration +//! to it. +//! +//! Each WebSocket stream implements the required `Stream` and `Sink` traits, +//! so the socket is just a stream of messages coming in and going out. + +#![deny(missing_docs, unused_must_use, unused_mut, unused_imports, unused_import_braces)] + +pub use tungstenite; + +mod compat; +#[cfg(feature = "connect")] +mod connect; +mod handshake; +#[cfg(feature = "stream")] +mod stream; +#[cfg(any(feature = "native-tls", feature = "__rustls-tls", feature = "connect"))] +mod tls; + +use std::io::{Read, Write}; + +use compat::{cvt, AllowStd, ContextWaker}; +use futures_util::{ + sink::{Sink, SinkExt}, + stream::{FusedStream, Stream}, +}; +use log::*; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; + +#[cfg(feature = "handshake")] +use tungstenite::{ + client::IntoClientRequest, + handshake::{ + client::{ClientHandshake, Response}, + server::{Callback, NoCallback}, + HandshakeError, + }, +}; +use tungstenite::{ + error::Error as WsError, + protocol::{Message, Role, WebSocket, WebSocketConfig}, +}; + +#[cfg(any(feature = "native-tls", feature = "__rustls-tls", feature = "connect"))] +pub use tls::Connector; +#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] +pub use tls::{client_async_tls, client_async_tls_with_config}; + +#[cfg(feature = "connect")] +pub use connect::{connect_async, connect_async_with_config}; + +#[cfg(all(any(feature = "native-tls", feature = "__rustls-tls"), feature = "connect"))] +pub use connect::connect_async_tls_with_config; + +#[cfg(feature = "stream")] +pub use stream::MaybeTlsStream; + +use tungstenite::protocol::CloseFrame; + +/// Creates a WebSocket handshake from a request and a stream. +/// For convenience, the user may call this with a url string, a URL, +/// or a `Request`. Calling with `Request` allows the user to add +/// a WebSocket protocol or other custom headers. +/// +/// Internally, this custom creates a handshake representation and returns +/// a future representing the resolution of the WebSocket handshake. The +/// returned future will resolve to either `WebSocketStream` or `Error` +/// depending on whether the handshake is successful. +/// +/// This is typically used for clients who have already established, for +/// example, a TCP connection to the remote server. +#[cfg(feature = "handshake")] +pub async fn client_async<'a, R, S>( + request: R, + stream: S, +) -> Result<(WebSocketStream, Response), WsError> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + client_async_with_config(request, stream, None).await +} + +/// The same as `client_async()` but the one can specify a websocket configuration. +/// Please refer to `client_async()` for more details. +#[cfg(feature = "handshake")] +pub async fn client_async_with_config<'a, R, S>( + request: R, + stream: S, + config: Option, +) -> Result<(WebSocketStream, Response), WsError> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin, +{ + let f = handshake::client_handshake(stream, move |allow_std| { + let request = request.into_client_request()?; + let cli_handshake = ClientHandshake::start(allow_std, request, config)?; + cli_handshake.handshake() + }); + f.await.map_err(|e| match e { + HandshakeError::Failure(e) => e, + e => WsError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())), + }) +} + +/// Accepts a new WebSocket connection with the provided stream. +/// +/// This function will internally call `server::accept` to create a +/// handshake representation and returns a future representing the +/// resolution of the WebSocket handshake. The returned future will resolve +/// to either `WebSocketStream` or `Error` depending if it's successful +/// or not. +/// +/// This is typically used after a socket has been accepted from a +/// `TcpListener`. That socket is then passed to this function to perform +/// the server half of the accepting a client's websocket connection. +#[cfg(feature = "handshake")] +pub async fn accept_async(stream: S) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin, +{ + accept_hdr_async(stream, NoCallback).await +} + +/// The same as `accept_async()` but the one can specify a websocket configuration. +/// Please refer to `accept_async()` for more details. +#[cfg(feature = "handshake")] +pub async fn accept_async_with_config( + stream: S, + config: Option, +) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin, +{ + accept_hdr_async_with_config(stream, NoCallback, config).await +} + +/// Accepts a new WebSocket connection with the provided stream. +/// +/// This function does the same as `accept_async()` but accepts an extra callback +/// for header processing. The callback receives headers of the incoming +/// requests and is able to add extra headers to the reply. +#[cfg(feature = "handshake")] +pub async fn accept_hdr_async(stream: S, callback: C) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin, + C: Callback + Unpin, +{ + accept_hdr_async_with_config(stream, callback, None).await +} + +/// The same as `accept_hdr_async()` but the one can specify a websocket configuration. +/// Please refer to `accept_hdr_async()` for more details. +#[cfg(feature = "handshake")] +pub async fn accept_hdr_async_with_config( + stream: S, + callback: C, + config: Option, +) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin, + C: Callback + Unpin, +{ + let f = handshake::server_handshake(stream, move |allow_std| { + tungstenite::accept_hdr_with_config(allow_std, callback, config) + }); + f.await.map_err(|e| match e { + HandshakeError::Failure(e) => e, + e => WsError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())), + }) +} + +/// A wrapper around an underlying raw stream which implements the WebSocket +/// protocol. +/// +/// A `WebSocketStream` represents a handshake that has been completed +/// successfully and both the server and the client are ready for receiving +/// and sending data. Message from a `WebSocketStream` are accessible +/// through the respective `Stream` and `Sink`. Check more information about +/// them in `futures-rs` crate documentation or have a look on the examples +/// and unit tests for this crate. +/// +/// # Cancel safety +/// +/// Reading messages is cancel-safe. `WebSocketStream` has no dedicated read +/// methods; messages arrive through its `Stream` implementation, and reading a +/// message via `StreamExt::next` follows that trait's cancel-safety: if the +/// `next()` future is dropped before it resolves (for example, as a branch of +/// `tokio::select!` that another branch completes first), no message is lost. +/// The next poll resumes from the same position in the stream. +/// +/// The `Sink` side (sending) does not carry a documented cancel-safety +/// guarantee. +#[derive(Debug)] +pub struct WebSocketStream { + inner: WebSocket>, + closing: bool, + ended: bool, + /// Tungstenite is probably ready to receive more data. + /// + /// `false` once start_send hits `WouldBlock` errors. + /// `true` initially and after `flush`ing. + ready: bool, +} + +impl WebSocketStream { + /// Convert a raw socket into a WebSocketStream without performing a + /// handshake. + pub async fn from_raw_socket(stream: S, role: Role, config: Option) -> Self + where + S: AsyncRead + AsyncWrite + Unpin, + { + handshake::without_handshake(stream, move |allow_std| { + WebSocket::from_raw_socket(allow_std, role, config) + }) + .await + } + + /// Convert a raw socket into a WebSocketStream without performing a + /// handshake. + pub async fn from_partially_read( + stream: S, + part: Vec, + role: Role, + config: Option, + ) -> Self + where + S: AsyncRead + AsyncWrite + Unpin, + { + handshake::without_handshake(stream, move |allow_std| { + WebSocket::from_partially_read(allow_std, part, role, config) + }) + .await + } + + pub(crate) fn new(ws: WebSocket>) -> Self { + Self { inner: ws, closing: false, ended: false, ready: true } + } + + fn with_context(&mut self, ctx: Option<(ContextWaker, &mut Context<'_>)>, f: F) -> R + where + S: Unpin, + F: FnOnce(&mut WebSocket>) -> R, + AllowStd: Read + Write, + { + trace!("{}:{} WebSocketStream.with_context", file!(), line!()); + if let Some((kind, ctx)) = ctx { + self.inner.get_mut().set_waker(kind, ctx.waker()); + } + f(&mut self.inner) + } + + /// Consumes the `WebSocketStream` and returns the underlying stream. + pub fn into_inner(self) -> S { + self.inner.into_inner().into_inner() + } + + /// Returns a shared reference to the inner stream. + pub fn get_ref(&self) -> &S + where + S: AsyncRead + AsyncWrite + Unpin, + { + self.inner.get_ref().get_ref() + } + + /// Returns a mutable reference to the inner stream. + pub fn get_mut(&mut self) -> &mut S + where + S: AsyncRead + AsyncWrite + Unpin, + { + self.inner.get_mut().get_mut() + } + + /// Returns a reference to the configuration of the tungstenite stream. + pub fn get_config(&self) -> &WebSocketConfig { + self.inner.get_config() + } + + /// Close the underlying web socket + pub async fn close(&mut self, msg: Option) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { + self.send(Message::Close(msg)).await + } +} + +impl Stream for WebSocketStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + trace!("{}:{} Stream.poll_next", file!(), line!()); + + // The connection has been closed or a critical error has occurred. + // We have already returned the error to the user, the `Stream` is unusable, + // so we assume that the stream has been "fused". + if self.ended { + return Poll::Ready(None); + } + + match futures_util::ready!(self.with_context(Some((ContextWaker::Read, cx)), |s| { + trace!("{}:{} Stream.with_context poll_next -> read()", file!(), line!()); + cvt(s.read()) + })) { + Ok(v) => Poll::Ready(Some(Ok(v))), + Err(e) => { + self.ended = true; + if matches!(e, WsError::AlreadyClosed | WsError::ConnectionClosed) { + Poll::Ready(None) + } else { + Poll::Ready(Some(Err(e))) + } + } + } + } +} + +impl FusedStream for WebSocketStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn is_terminated(&self) -> bool { + self.ended + } +} + +impl Sink for WebSocketStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + type Error = WsError; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self.ready { + Poll::Ready(Ok(())) + } else { + // Currently blocked so try to flush the blockage away + (*self).with_context(Some((ContextWaker::Write, cx)), |s| cvt(s.flush())).map(|r| { + self.ready = true; + r + }) + } + } + + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + match (*self).with_context(None, |s| s.write(item)) { + Ok(()) => { + self.ready = true; + Ok(()) + } + Err(WsError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { + // the message was accepted and queued so not an error + // but `poll_ready` will now start trying to flush the block + self.ready = false; + Ok(()) + } + Err(e) => { + self.ready = true; + debug!("websocket start_send error: {}", e); + Err(e) + } + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + (*self).with_context(Some((ContextWaker::Write, cx)), |s| cvt(s.flush())).map(|r| { + self.ready = true; + match r { + // WebSocket connection has just been closed. Flushing completed, not an error. + Err(WsError::ConnectionClosed) => Ok(()), + other => other, + } + }) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.ready = true; + let res = if self.closing { + // After queueing it, we call `flush` to drive the close handshake to completion. + (*self).with_context(Some((ContextWaker::Write, cx)), |s| s.flush()) + } else { + (*self).with_context(Some((ContextWaker::Write, cx)), |s| s.close(None)) + }; + + match res { + Ok(()) => Poll::Ready(Ok(())), + Err(WsError::ConnectionClosed) => Poll::Ready(Ok(())), + Err(WsError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { + trace!("WouldBlock"); + self.closing = true; + Poll::Pending + } + Err(err) => { + debug!("websocket close error: {}", err); + Poll::Ready(Err(err)) + } + } + } +} + +/// Get a domain from an URL. +#[cfg(any(feature = "connect", feature = "native-tls", feature = "__rustls-tls"))] +#[inline] +fn domain(request: &tungstenite::handshake::client::Request) -> Result { + match request.uri().host() { + // rustls expects IPv6 addresses without the surrounding [] brackets + #[cfg(feature = "__rustls-tls")] + Some(d) if d.starts_with('[') && d.ends_with(']') => Ok(d[1..d.len() - 1].to_string()), + Some(d) => Ok(d.to_string()), + None => Err(WsError::Url(tungstenite::error::UrlError::NoHostName)), + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "connect")] + use crate::stream::MaybeTlsStream; + use crate::{compat::AllowStd, WebSocketStream}; + use std::io::{Read, Write}; + #[cfg(feature = "connect")] + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + fn is_read() {} + fn is_write() {} + #[cfg(feature = "connect")] + fn is_async_read() {} + #[cfg(feature = "connect")] + fn is_async_write() {} + fn is_unpin() {} + + #[test] + fn web_socket_stream_has_traits() { + is_read::>(); + is_write::>(); + + #[cfg(feature = "connect")] + is_async_read::>(); + #[cfg(feature = "connect")] + is_async_write::>(); + + is_unpin::>(); + #[cfg(feature = "connect")] + is_unpin::>>(); + } +} diff --git a/vendor/tokio-tungstenite/src/stream.rs b/vendor/tokio-tungstenite/src/stream.rs new file mode 100644 index 00000000..67985c7d --- /dev/null +++ b/vendor/tokio-tungstenite/src/stream.rs @@ -0,0 +1,93 @@ +//! Convenience wrapper for streams to switch between plain TCP and TLS at runtime. +//! +//! There is no dependency on actual TLS implementations. Everything like +//! `native_tls` or `openssl` will work as long as there is a TLS stream supporting standard +//! `Read + Write` traits. +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +/// A stream that might be protected with TLS. +#[non_exhaustive] +#[derive(Debug)] +pub enum MaybeTlsStream { + /// Unencrypted socket stream. + Plain(S), + /// Encrypted socket stream using `native-tls`. + #[cfg(feature = "native-tls")] + NativeTls(tokio_native_tls::TlsStream), + /// Encrypted socket stream using `rustls`. + #[cfg(feature = "__rustls-tls")] + Rustls(tokio_rustls::client::TlsStream), +} + +impl MaybeTlsStream { + /// Returns a shared reference to the inner stream. + pub fn get_ref(&self) -> &S { + match self { + MaybeTlsStream::Plain(s) => s, + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(s) => s.get_ref().get_ref().get_ref(), + #[cfg(feature = "__rustls-tls")] + MaybeTlsStream::Rustls(s) => s.get_ref().0, + } + } +} + +impl AsyncRead for MaybeTlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Plain(ref mut s) => Pin::new(s).poll_read(cx, buf), + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(s) => Pin::new(s).poll_read(cx, buf), + #[cfg(feature = "__rustls-tls")] + MaybeTlsStream::Rustls(s) => Pin::new(s).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for MaybeTlsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Plain(ref mut s) => Pin::new(s).poll_write(cx, buf), + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(s) => Pin::new(s).poll_write(cx, buf), + #[cfg(feature = "__rustls-tls")] + MaybeTlsStream::Rustls(s) => Pin::new(s).poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Plain(ref mut s) => Pin::new(s).poll_flush(cx), + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(s) => Pin::new(s).poll_flush(cx), + #[cfg(feature = "__rustls-tls")] + MaybeTlsStream::Rustls(s) => Pin::new(s).poll_flush(cx), + } + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Plain(ref mut s) => Pin::new(s).poll_shutdown(cx), + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(s) => Pin::new(s).poll_shutdown(cx), + #[cfg(feature = "__rustls-tls")] + MaybeTlsStream::Rustls(s) => Pin::new(s).poll_shutdown(cx), + } + } +} diff --git a/vendor/tokio-tungstenite/src/tls.rs b/vendor/tokio-tungstenite/src/tls.rs new file mode 100644 index 00000000..9ad9e67b --- /dev/null +++ b/vendor/tokio-tungstenite/src/tls.rs @@ -0,0 +1,237 @@ +//! Connection helper. +use tokio::io::{AsyncRead, AsyncWrite}; + +use tungstenite::{ + client::uri_mode, error::Error, handshake::client::Response, protocol::WebSocketConfig, +}; + +use crate::{client_async_with_config, IntoClientRequest, WebSocketStream}; + +pub use crate::stream::MaybeTlsStream; + +/// A connector that can be used when establishing connections, allowing to control whether +/// `native-tls` or `rustls` is used to create a TLS connection. Or TLS can be disabled with the +/// `Plain` variant. +#[non_exhaustive] +#[derive(Clone)] +pub enum Connector { + /// Plain (non-TLS) connector. + Plain, + /// `native-tls` TLS connector. + #[cfg(feature = "native-tls")] + NativeTls(native_tls_crate::TlsConnector), + /// `rustls` TLS connector. + #[cfg(feature = "__rustls-tls")] + Rustls(std::sync::Arc), +} + +mod encryption { + #[cfg(feature = "native-tls")] + pub mod native_tls { + use native_tls_crate::TlsConnector; + use tokio_native_tls::TlsConnector as TokioTlsConnector; + + use tokio::io::{AsyncRead, AsyncWrite}; + + use tungstenite::{error::TlsError, stream::Mode, Error}; + + use crate::stream::MaybeTlsStream; + + pub async fn wrap_stream( + socket: S, + domain: String, + mode: Mode, + tls_connector: Option, + ) -> Result, Error> + where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + { + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => { + let try_connector = tls_connector.map_or_else(TlsConnector::new, Ok); + let connector = try_connector.map_err(TlsError::from)?; + let stream = TokioTlsConnector::from(connector); + let connected = stream.connect(&domain, socket).await; + match connected { + Err(e) => Err(Error::Tls(e.into())), + Ok(s) => Ok(MaybeTlsStream::NativeTls(s)), + } + } + } + } + } + + #[cfg(feature = "__rustls-tls")] + pub mod rustls { + pub use rustls::ClientConfig; + use rustls::RootCertStore; + use rustls_pki_types::ServerName; + use tokio_rustls::TlsConnector as TokioTlsConnector; + + use std::{convert::TryFrom, sync::Arc}; + use tokio::io::{AsyncRead, AsyncWrite}; + + use tungstenite::{error::TlsError, stream::Mode, Error}; + + use crate::stream::MaybeTlsStream; + + pub async fn wrap_stream( + socket: S, + domain: String, + mode: Mode, + tls_connector: Option>, + ) -> Result, Error> + where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + { + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => { + let config = match tls_connector { + Some(config) => config, + None => { + #[allow(unused_mut)] + let mut root_store = RootCertStore::empty(); + #[cfg(feature = "rustls-tls-native-roots")] + { + let rustls_native_certs::CertificateResult { + certs, errors, .. + } = rustls_native_certs::load_native_certs(); + + if !errors.is_empty() { + log::warn!( + "native root CA certificate loading errors: {errors:?}" + ); + } + + // Not finding any native root CA certificates is not fatal if the + // "rustls-tls-webpki-roots" feature is enabled. + #[cfg(not(feature = "rustls-tls-webpki-roots"))] + if certs.is_empty() { + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("no native root CA certificates found (errors: {errors:?})")).into()); + } + + let total_number = certs.len(); + let (number_added, number_ignored) = + root_store.add_parsable_certificates(certs); + log::debug!("Added {number_added}/{total_number} native root certificates (ignored {number_ignored})"); + } + #[cfg(feature = "rustls-tls-webpki-roots")] + { + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + } + + Arc::new( + ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(), + ) + } + }; + let domain = ServerName::try_from(domain.as_str()) + .map_err(|_| TlsError::InvalidDnsName)? + .to_owned(); + let stream = TokioTlsConnector::from(config); + let connected = stream.connect(domain, socket).await; + + match connected { + Err(e) => Err(Error::Io(e)), + Ok(s) => Ok(MaybeTlsStream::Rustls(s)), + } + } + } + } + } + + pub mod plain { + use tokio::io::{AsyncRead, AsyncWrite}; + + use tungstenite::{ + error::{Error, UrlError}, + stream::Mode, + }; + + use crate::stream::MaybeTlsStream; + + pub async fn wrap_stream(socket: S, mode: Mode) -> Result, Error> + where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + { + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => Err(Error::Url(UrlError::TlsFeatureNotEnabled)), + } + } + } +} + +/// Creates a WebSocket handshake from a request and a stream, +/// upgrading the stream to TLS if required. +#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] +pub async fn client_async_tls( + request: R, + stream: S, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + MaybeTlsStream: Unpin, +{ + client_async_tls_with_config(request, stream, None, None).await +} + +/// The same as `client_async_tls()` but the one can specify a websocket configuration, +/// and an optional connector. If no connector is specified, a default one will +/// be created. +/// +/// Please refer to `client_async_tls()` for more details. +pub async fn client_async_tls_with_config( + request: R, + stream: S, + config: Option, + connector: Option, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + MaybeTlsStream: Unpin, +{ + let request = request.into_client_request()?; + + #[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] + let domain = crate::domain(&request)?; + + // Make sure we check domain and mode first. URL must be valid. + let mode = uri_mode(request.uri())?; + + let stream = match connector { + Some(conn) => match conn { + #[cfg(feature = "native-tls")] + Connector::NativeTls(conn) => { + self::encryption::native_tls::wrap_stream(stream, domain, mode, Some(conn)).await + } + #[cfg(feature = "__rustls-tls")] + Connector::Rustls(conn) => { + self::encryption::rustls::wrap_stream(stream, domain, mode, Some(conn)).await + } + Connector::Plain => self::encryption::plain::wrap_stream(stream, mode).await, + }, + None => { + #[cfg(feature = "native-tls")] + { + self::encryption::native_tls::wrap_stream(stream, domain, mode, None).await + } + #[cfg(all(feature = "__rustls-tls", not(feature = "native-tls")))] + { + self::encryption::rustls::wrap_stream(stream, domain, mode, None).await + } + #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))] + { + self::encryption::plain::wrap_stream(stream, mode).await + } + } + }?; + + client_async_with_config(request, stream, config).await +} diff --git a/vendor/webrtc-sys-build/Cargo.toml b/vendor/webrtc-sys-build/Cargo.toml deleted file mode 100644 index 30d872e1..00000000 --- a/vendor/webrtc-sys-build/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -# Upstream's normalized manifest; only [dependencies.zip] changed, see README.md. - -[package] -edition = "2021" -name = "webrtc-sys-build" -version = "0.3.19" -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Build utilities when working with libwebrtc" -readme = false -license = "Apache-2.0" -repository = "https://github.com/livekit/rust-sdks" - -[lib] -name = "webrtc_sys_build" -path = "src/lib.rs" - -[dependencies.anyhow] -version = "1.0" - -[dependencies.fs2] -version = "0.4" - -[dependencies.regex] -version = "1.0" - -[dependencies.reqwest] -version = "0.12" -features = [ - "rustls-tls-native-roots", - "blocking", -] -default-features = false - -[dependencies.scratch] -version = "1.0" - -[dependencies.semver] -version = "1.0" - -[dependencies.zip] -version = "9.0" -features = ["deflate-flate2-zlib-rs"] -default-features = false diff --git a/vendor/webrtc-sys-build/README.md b/vendor/webrtc-sys-build/README.md deleted file mode 100644 index 8b88a7b9..00000000 --- a/vendor/webrtc-sys-build/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Vendored from [`webrtc-sys-build` 0.3.19](https://crates.io/crates/webrtc-sys-build), -upstream revision `2d9f01ab1e933a86a8a5c53805ee29ee58b9be1b`, -`webrtc-sys/build/`. Licensed under Apache-2.0; see LICENSE. - -Only the ZIP dependency changes: 0.6 to 9.0, with just the zlib-rs -Deflate decoder since the helper only extracts, fixing RUSTSEC-2026-0367, -RUSTSEC-2026-0368 and RUSTSEC-2026-0369. The upstream Rust source is unchanged. -Remove the Cargo patch and this directory when upstream releases a build -helper using ZIP 9 or later. diff --git a/vendor/webrtc-sys-build/src/lib.rs b/vendor/webrtc-sys-build/src/lib.rs deleted file mode 100644 index 586b6675..00000000 --- a/vendor/webrtc-sys-build/src/lib.rs +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2025 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::PathBuf; -use std::{ - collections::HashSet, - env, - fs::{self, File}, - io::{self, BufRead, Write}, - path, - process::Command, -}; - -use anyhow::{anyhow, Context, Result}; -use fs2::FileExt; -use regex::Regex; -use reqwest::StatusCode; - -pub const SCRATH_PATH: &str = "livekit_webrtc"; -pub const WEBRTC_TAG: &str = "webrtc-89d790b"; -pub const IGNORE_DEFINES: [&str; 2] = ["CR_CLANG_REVISION", "CR_XCODE_VERSION"]; - -pub fn target_os() -> String { - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - let target = env::var("TARGET").unwrap(); - let is_simulator = target.ends_with("-sim"); - - match target_os.as_str() { - "windows" => "win", - "macos" => "mac", - "ios" => { - if is_simulator { - "ios-simulator" - } else { - "ios-device" - } - } - _ => &target_os, - } - .to_string() -} - -pub fn target_arch() -> String { - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - match target_arch.as_str() { - "aarch64" => "arm64", - "x86_64" => "x64", - _ => &target_arch, - } - .to_owned() -} - -/// The full name of the webrtc library -/// e.g. mac-x64-release (Same name on GH releases) -pub fn webrtc_triple() -> String { - let profile = if use_debug() { "debug" } else { "release" }; - format!("{}-{}-{}", target_os(), target_arch(), profile) -} - -/// Using debug builds of webrtc is still experimental for now -/// On Windows, Rust doesn't link against libcmtd on debug, which is an issue -/// Default to false (even on cargo debug) -pub fn use_debug() -> bool { - let var = env::var("LK_DEBUG_WEBRTC"); - var.is_ok() && var.unwrap() == "true" -} - -/// The location of the custom build is defined by the user -pub fn custom_dir() -> Option { - if let Ok(path) = env::var("LK_CUSTOM_WEBRTC") { - return Some(path::PathBuf::from(path)); - } - None -} - -/// Location of the downloaded webrtc binaries -/// The reason why we don't use OUT_DIR is because we sometimes need to share the same binaries -/// across multiple crates without dependencies constraints -/// This also has the benefit of not re-downloading the binaries for each crate -pub fn prebuilt_dir() -> path::PathBuf { - let target_dir = scratch::path(SCRATH_PATH); - path::Path::new(&target_dir).join(format!( - "livekit/{}-{}/{}", - webrtc_triple(), - WEBRTC_TAG, - webrtc_triple() - )) -} - -pub fn download_url() -> String { - format!( - "https://github.com/livekit/rust-sdks/releases/download/{}/{}.zip", - WEBRTC_TAG, - format!("webrtc-{}", webrtc_triple()) - ) -} - -/// Used location of libwebrtc depending on whether it's a custom build or not -pub fn webrtc_dir() -> path::PathBuf { - if let Some(path) = custom_dir() { - return path; - } - - prebuilt_dir() -} - -pub fn webrtc_defines() -> Vec<(String, Option)> { - // read preprocessor definitions from webrtc.ninja - let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap(); - let mut files = vec![webrtc_dir().join("webrtc.ninja")]; - // include desktop_capture.ninja to avoid ABI mismatch for DesktopCaptureOptions due to WEBRTC_USE_X11 missing - // libwebrtc does not implement desktop capture on Android - if env::var("CARGO_CFG_TARGET_OS").unwrap() != "android" { - files.push(webrtc_dir().join("desktop_capture.ninja")); - } - - let mut seen = HashSet::new(); - let mut vec = Vec::new(); - - for path in files { - let gni = fs::File::open(&path) - .unwrap_or_else(|e| panic!("Could not open ninja file: {path:?}\n{e:?}")); - - let mut defines_line = String::default(); - io::BufReader::new(gni).read_line(&mut defines_line).unwrap(); - for cap in defines_re.captures_iter(&defines_line) { - let define_name = &cap[1]; - let define_value = cap.get(2).map(|m| m.as_str()); - if IGNORE_DEFINES.contains(&define_name) { - continue; - } - let value = define_value.map(str::to_string); - let name = define_name.to_owned(); - if seen.insert((name.clone(), value.clone())) { - vec.push((name, value)); - } - } - } - - vec -} - -pub fn configure_jni_symbols() -> Result<()> { - download_webrtc().context("Failed to download WebRTC binaries for JNI configuration")?; - - let toolchain = android_ndk_toolchain().context("Failed to locate Android NDK toolchain")?; - let toolchain_bin = toolchain.join("bin"); - - let webrtc_dir = webrtc_dir(); - let webrtc_lib = webrtc_dir.join("lib"); - - let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap()); - - // Find JNI symbols - let readelf_output = Command::new(toolchain_bin.join("llvm-readelf")) - .arg("-Ws") - .arg(webrtc_lib.join("libwebrtc.a")) - .output() - .expect("failed to run llvm-readelf"); - - let jni_regex = Regex::new(r"(Java_livekit_org_webrtc.*)").unwrap(); - let content = String::from_utf8_lossy(&readelf_output.stdout); - let jni_symbols: Vec<&str> = - jni_regex.captures_iter(&content).map(|cap| cap.get(1).unwrap().as_str()).collect(); - - if jni_symbols.is_empty() { - return Err(anyhow!("No JNI symbols found")); // Shouldn't happen - } - - // Keep JNI symbols - for symbol in &jni_symbols { - println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol); - } - - // Version script - let vs_path = out_dir.join("webrtc_jni.map"); - let mut vs_file = fs::File::create(&vs_path).context("Failed to create version script file")?; - - let jni_symbols = jni_symbols.join("; "); - write!(vs_file, "JNI_WEBRTC {{\n\tglobal: {}; \n}};", jni_symbols) - .context("Failed to write version script")?; - - println!("cargo:rustc-link-arg=-Wl,--version-script={}", vs_path.display()); - - Ok(()) -} - -pub fn download_webrtc() -> Result<()> { - let dir = scratch::path(SCRATH_PATH); - // temporary fix to avoid github workflow issue - fs::create_dir_all(&dir).context("Failed to create scratch_path")?; - let flock = File::create(dir.join(".lock")) - .context("Failed to create lock file for WebRTC download")?; - flock.lock_exclusive().context("Failed to acquire exclusive lock for WebRTC download")?; - - let webrtc_dir = webrtc_dir(); - if webrtc_dir.exists() { - return Ok(()); - } - - let mut resp = reqwest::blocking::get(download_url()); - for attempt in 1..3 { - let transient = match &resp { - Ok(resp) => { - resp.status().is_server_error() || resp.status() == StatusCode::TOO_MANY_REQUESTS - } - Err(_) => true, - }; - if !transient { - break; - } - println!("cargo:warning=webrtc download attempt {} failed, retrying in 5s...", attempt); - std::thread::sleep(std::time::Duration::from_secs(5)); - resp = reqwest::blocking::get(download_url()); - } - let mut resp = resp.context("Failed to send HTTP request to download WebRTC")?; - if resp.status() != StatusCode::OK { - return Err(anyhow!("failed to download webrtc: {}", resp.status())); - } - - let out_dir = env::var("OUT_DIR").unwrap(); - let tmp_path = PathBuf::from(out_dir).join("webrtc.zip"); - let mut file = fs::File::options() - .write(true) - .read(true) - .create(true) - .open(&tmp_path) - .context("Failed to create temporary file for WebRTC download")?; - resp.copy_to(&mut file).context("Failed to write WebRTC download to temporary file")?; - - // Extract into a sibling temp dir, then atomically rename into place so concurrent - // observers see either no `webrtc_dir` or a fully-populated one — never the partially- - // extracted state that made `fs::copy(webrtc_dir/LICENSE.md, …)` in callers flaky. - let tmp_extract = webrtc_dir.parent().unwrap().join(format!(".{}.tmp", webrtc_triple())); - let _ = fs::remove_dir_all(&tmp_extract); // clean up leftover from a crashed build - fs::create_dir_all(&tmp_extract).context("Failed to create temp extraction dir")?; - - let mut archive = zip::ZipArchive::new(file).context("Failed to open WebRTC zip archive")?; - archive.extract(&tmp_extract).context("Failed to extract WebRTC archive")?; - drop(archive); - - // The zip root is `{triple}/`, so extracted content sits at `tmp_extract/{triple}/`. - fs::rename(tmp_extract.join(webrtc_triple()), &webrtc_dir) - .context("Failed to move extracted WebRTC into place")?; - let _ = fs::remove_dir_all(&tmp_extract); - - #[cfg(unix)] - ensure_owner_readable(&webrtc_dir).context("Failed to normalize libwebrtc file permissions")?; - - fs::remove_file(&tmp_path).context("Failed to remove temporary WebRTC zip file")?; - Ok(()) -} - -#[cfg(unix)] -fn ensure_owner_readable(root: &path::Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - fn walk(p: &path::Path) -> Result<()> { - let meta = fs::symlink_metadata(p)?; - // Don't try to chmod through symlinks — follow them only via the - // entries we walk into normally. set_permissions on a symlink may - // affect the target on some platforms; just skip. - if meta.file_type().is_symlink() { - return Ok(()); - } - let mut perms = meta.permissions(); - let mut mode = perms.mode(); - mode |= 0o600; // owner read+write - if meta.is_dir() { - mode |= 0o100; // owner traverse - } - perms.set_mode(mode); - // Best-effort: if the file is on a read-only mount or owned by another - // user (shouldn't happen here), we just skip rather than failing the - // build over a permission tweak. - let _ = fs::set_permissions(p, perms); - if meta.is_dir() { - for entry in fs::read_dir(p)? { - walk(&entry?.path())?; - } - } - Ok(()) - } - walk(root) -} - -pub fn android_ndk_toolchain() -> Result { - let host_os = host_os(); - - let home = env::var("HOME"); - let local = env::var("LOCALAPPDATA"); - - let home = if host_os == Some("linux") { - path::PathBuf::from(home.unwrap()) - } else if host_os == Some("darwin") { - path::PathBuf::from(home.unwrap()).join("Library") - } else if host_os == Some("windows") { - path::PathBuf::from(local.unwrap()) - } else { - return Err(anyhow!("Unsupported host OS")); - }; - - let ndk_dir = || -> Option { - let ndk_env = env::var("ANDROID_NDK_HOME"); - if let Ok(ndk_env) = ndk_env { - return Some(path::PathBuf::from(ndk_env)); - } - - let ndk_dir = home.join("Android/sdk/ndk"); - if !ndk_dir.exists() { - return None; - } - - // Find the highest version - let versions = fs::read_dir(ndk_dir.clone()); - if versions.is_err() { - return None; - } - - let version = versions - .unwrap() - .filter_map(Result::ok) - .filter_map(|dir| dir.file_name().to_str().map(ToOwned::to_owned)) - .filter_map(|dir| semver::Version::parse(&dir).ok()) - .max_by(semver::Version::cmp); - - version.as_ref()?; - - let version = version.unwrap(); - Some(ndk_dir.join(version.to_string())) - }(); - - if let Some(ndk_dir) = ndk_dir { - let llvm_dir = if host_os == Some("linux") { - "linux-x86_64" - } else if host_os == Some("darwin") { - "darwin-x86_64" - } else if host_os == Some("windows") { - "windows-x86_64" - } else { - return Err(anyhow!("Unsupported host OS")); - }; - - Ok(ndk_dir.join(format!("toolchains/llvm/prebuilt/{}", llvm_dir))) - } else { - Err(anyhow!("Android NDK not found, please set ANDROID_NDK_HOME to your NDK path")) - } -} - -fn host_os() -> Option<&'static str> { - let host = env::var("HOST").unwrap(); - if host.contains("darwin") { - Some("darwin") - } else if host.contains("linux") { - Some("linux") - } else if host.contains("windows") { - Some("windows") - } else { - None - } -}