diff --git a/Cargo.lock b/Cargo.lock index c2742ef..035951b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,7 +48,7 @@ dependencies = [ "android-properties", "bitflags 2.11.0", "cc", - "jni", + "jni 0.22.4", "libc", "log", "ndk", @@ -142,6 +142,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -256,6 +262,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -316,6 +328,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -329,7 +351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -342,7 +364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -394,6 +416,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -477,6 +508,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand", + "siphasher", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -566,6 +609,19 @@ dependencies = [ "windows-link", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -573,9 +629,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -688,6 +746,22 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -802,6 +876,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.15" @@ -841,6 +921,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -904,6 +990,17 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "ndk" version = "0.9.0" @@ -955,6 +1052,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1213,6 +1316,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "orbclient" version = "0.3.51" @@ -1235,6 +1344,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1324,6 +1443,12 @@ dependencies = [ "serde", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1389,6 +1514,63 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1445,6 +1627,19 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1480,12 +1675,32 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1521,6 +1736,80 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.21.1", + "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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1536,6 +1825,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1548,6 +1846,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1639,6 +1960,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -1724,6 +2051,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -1739,6 +2076,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -1853,6 +2196,65 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -1987,6 +2389,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "valuable" version = "0.1.1" @@ -2009,6 +2417,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -2293,6 +2707,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2308,13 +2731,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2323,7 +2755,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2335,34 +2767,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2375,24 +2840,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2414,7 +2903,7 @@ dependencies = [ "calloop 0.13.0", "cfg_aliases", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics", "cursor-icon", "dpi", @@ -2561,7 +3050,13 @@ name = "wrclient" version = "0.1.0" dependencies = [ "miette", + "quinn", + "rcgen", + "rustls", + "serde", + "tokio", "tracing", + "tracing-subscriber", "wayray-protocol", ] @@ -2571,8 +3066,13 @@ version = "0.1.0" dependencies = [ "ctrlc", "miette", + "quinn", + "rcgen", + "rustls", + "serde", "smithay", "thiserror 2.0.18", + "tokio", "tracing", "tracing-subscriber", "wayray-protocol", @@ -2652,6 +3152,15 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -2672,6 +3181,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index f9c2264..aead0b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,7 @@ thiserror = "2" serde = { version = "1", features = ["derive"] } postcard = { version = "1", features = ["alloc"] } zstd = "0.13" +quinn = "0.11" +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +rcgen = "0.13" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync"] } diff --git a/crates/wrclient/Cargo.toml b/crates/wrclient/Cargo.toml index 9e64e1a..b303850 100644 --- a/crates/wrclient/Cargo.toml +++ b/crates/wrclient/Cargo.toml @@ -7,4 +7,12 @@ license.workspace = true [dependencies] wayray-protocol.workspace = true tracing.workspace = true +tracing-subscriber.workspace = true miette.workspace = true +serde.workspace = true +quinn.workspace = true +rustls.workspace = true +tokio.workspace = true + +[dev-dependencies] +rcgen.workspace = true diff --git a/crates/wrclient/src/main.rs b/crates/wrclient/src/main.rs index 558c003..f989222 100644 --- a/crates/wrclient/src/main.rs +++ b/crates/wrclient/src/main.rs @@ -1,3 +1,5 @@ +pub mod network; + fn main() { println!("wrclient viewer"); } diff --git a/crates/wrclient/src/network.rs b/crates/wrclient/src/network.rs new file mode 100644 index 0000000..22ca35a --- /dev/null +++ b/crates/wrclient/src/network.rs @@ -0,0 +1,395 @@ +//! QUIC transport client for wrclient. +//! +//! Connects to a wrsrvd server, establishes three logical streams: +//! - **Control** (bidirectional): handshake, ping/pong, frame acks +//! - **Display** (server→client, unidirectional): frame updates +//! - **Input** (client→server, unidirectional): keyboard/pointer events +//! +//! ## Quinn stream semantics +//! +//! Quinn streams are lazily materialized on the wire: the peer's +//! `accept_bi()`/`accept_uni()` won't resolve until data is written. +//! The handshake protocol accounts for this by having each side write +//! data before the other side tries to accept. + +use std::net::SocketAddr; + +use quinn::rustls::pki_types::CertificateDer; +use tracing::info; +use wayray_protocol::codec; +use wayray_protocol::messages::{ + ClientHello, ControlMessage, DisplayMessage, InputMessage, ServerHello, +}; + +/// Configuration for the QUIC client connection. +pub struct ClientConfig { + /// Server address to connect to. + pub server_addr: SocketAddr, + /// Client capabilities to advertise in the hello. + pub capabilities: Vec, +} + +impl Default for ClientConfig { + fn default() -> Self { + Self { + server_addr: "127.0.0.1:4433".parse().unwrap(), + capabilities: vec!["display".to_string()], + } + } +} + +/// An established connection to a wrsrvd server with all streams ready. +/// +/// After `connect()`, the control stream and input stream are ready to use. +/// The display stream is accepted lazily when the server sends the first frame. +pub struct ServerConnection { + /// The underlying QUIC connection. + pub connection: quinn::Connection, + /// Bidirectional control stream -- send side. + pub control_send: quinn::SendStream, + /// Bidirectional control stream -- receive side. + pub control_recv: quinn::RecvStream, + /// Unidirectional input stream to server (send only). + pub input_send: quinn::SendStream, + /// The server hello received during handshake. + pub server_hello: ServerHello, +} + +impl ServerConnection { + /// Send a frame acknowledgment to the server. + pub async fn send_frame_ack( + &mut self, + sequence: u64, + ) -> Result<(), Box> { + let msg = ControlMessage::FrameAck(wayray_protocol::messages::FrameAck { sequence }); + write_message(&mut self.control_send, &msg).await + } + + /// Send an input message to the server. + pub async fn send_input( + &mut self, + input: &InputMessage, + ) -> Result<(), Box> { + write_message(&mut self.input_send, input).await + } + + /// Accept the display stream from the server and read the next frame. + /// + /// On first call, accepts the unidirectional stream from the server. + /// The server triggers this by writing the first frame update. + pub async fn accept_display_stream( + &mut self, + ) -> Result> { + let recv = self.connection.accept_uni().await?; + info!("display stream accepted"); + Ok(recv) + } + + /// Read the next control message from the server. + pub async fn recv_control(&mut self) -> Result> { + read_message(&mut self.control_recv).await + } + + /// Send a pong response. + pub async fn send_pong(&mut self, timestamp: u64) -> Result<(), Box> { + let msg = ControlMessage::Pong(wayray_protocol::messages::Pong { timestamp }); + write_message(&mut self.control_send, &msg).await + } +} + +/// Dummy certificate verifier that accepts any server cert. +/// +/// Used during development when servers use self-signed certificates. +/// TODO: Replace with proper certificate pinning or CA verification. +#[derive(Debug)] +struct SkipServerVerification; + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +/// Build a quinn client config that skips certificate verification. +fn build_client_config() -> quinn::ClientConfig { + let provider = rustls::crypto::ring::default_provider(); + let crypto = rustls::ClientConfig::builder_with_provider(provider.into()) + .with_safe_default_protocol_versions() + .expect("TLS protocol versions") + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(SkipServerVerification)) + .with_no_client_auth(); + let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(crypto) + .expect("QUIC client crypto config"); + quinn::ClientConfig::new(std::sync::Arc::new(crypto)) +} + +/// Connect to a wrsrvd server and perform the handshake. +/// +/// Returns a `ServerConnection` with the control and input streams ready. +/// The display stream is accepted lazily via `accept_display_stream()`. +/// +/// The caller must keep the returned `quinn::Endpoint` alive for the +/// duration of the connection. +pub async fn connect( + config: &ClientConfig, +) -> Result<(quinn::Endpoint, ServerConnection), Box> { + let client_config = build_client_config(); + + let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse::()?)?; + endpoint.set_default_client_config(client_config); + + info!(server = %config.server_addr, "connecting to wrsrvd"); + let connection = endpoint.connect(config.server_addr, "localhost")?.await?; + info!("QUIC connection established"); + + // Open control stream (bidirectional) and immediately send ClientHello. + // Writing data triggers the server's accept_bi(). + let (mut control_send, mut control_recv) = connection.open_bi().await?; + info!("control stream opened"); + + let client_hello = ControlMessage::ClientHello(ClientHello { + version: wayray_protocol::PROTOCOL_VERSION, + capabilities: config.capabilities.clone(), + }); + write_message(&mut control_send, &client_hello).await?; + info!("sent ClientHello"); + + // Read ServerHello response. + let response: ControlMessage = read_message(&mut control_recv).await?; + let server_hello = match response { + ControlMessage::ServerHello(hello) => { + info!( + version = hello.version, + session_id = hello.session_id, + width = hello.output_width, + height = hello.output_height, + "received ServerHello" + ); + hello + } + other => { + return Err(format!("expected ServerHello, got {other:?}").into()); + } + }; + + // Open input stream (unidirectional to server). + // Data written later via send_input() triggers the server's accept_uni(). + let input_send = connection.open_uni().await?; + info!("input stream opened"); + + Ok(( + endpoint, + ServerConnection { + connection, + control_send, + control_recv, + input_send, + server_hello, + }, + )) +} + +/// Read a length-prefixed message from a QUIC receive stream. +async fn read_message( + recv: &mut quinn::RecvStream, +) -> Result> { + let mut len_buf = [0u8; 4]; + recv.read_exact(&mut len_buf).await?; + let len = u32::from_le_bytes(len_buf) as usize; + + let mut payload = vec![0u8; len]; + recv.read_exact(&mut payload).await?; + + let msg = codec::decode(&payload)?; + Ok(msg) +} + +/// Write a length-prefixed message to a QUIC send stream. +async fn write_message( + send: &mut quinn::SendStream, + msg: &T, +) -> Result<(), Box> { + let encoded = codec::encode(msg)?; + send.write_all(&encoded).await?; + Ok(()) +} + +/// Read a frame update from a display receive stream. +pub async fn read_display_message( + recv: &mut quinn::RecvStream, +) -> Result> { + read_message(recv).await +} + +#[cfg(test)] +mod tests { + use super::*; + use wayray_protocol::messages::{FrameUpdate, KeyState, KeyboardEvent}; + + /// Helper: start a test server on an ephemeral port, return its address. + async fn start_test_server() -> (quinn::Endpoint, SocketAddr) { + let CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(vec!["localhost".to_string(), "wayray".to_string()]) + .unwrap(); + let cert_der = CertificateDer::from(cert); + let key_der = + quinn::rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der()).unwrap(); + + let provider = rustls::crypto::ring::default_provider(); + let crypto = rustls::ServerConfig::builder_with_provider(provider.into()) + .with_safe_default_protocol_versions() + .expect("TLS protocol versions") + .with_no_client_auth() + .with_single_cert(vec![cert_der], key_der) + .unwrap(); + let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(crypto).unwrap(); + let server_config = quinn::ServerConfig::with_crypto(std::sync::Arc::new(crypto)); + + let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); + let endpoint = quinn::Endpoint::server(server_config, addr).unwrap(); + let actual_addr = endpoint.local_addr().unwrap(); + (endpoint, actual_addr) + } + + use rcgen::CertifiedKey; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn client_connect_and_handshake() { + let (endpoint, addr) = start_test_server().await; + + // Server side: accept bi (triggered by client's ClientHello write), + // read ClientHello, send ServerHello. + let server = tokio::spawn(async move { + let incoming = endpoint.accept().await.unwrap(); + let connection = incoming.await.unwrap(); + + let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap(); + + let hello: ControlMessage = read_message(&mut control_recv).await.unwrap(); + assert!(matches!(hello, ControlMessage::ClientHello(_))); + + let server_hello = ControlMessage::ServerHello(ServerHello { + version: wayray_protocol::PROTOCOL_VERSION, + session_id: 99, + output_width: 1920, + output_height: 1080, + }); + write_message(&mut control_send, &server_hello) + .await + .unwrap(); + + // Wait for the client to finish setup (it opens input uni). + // Read next control message or wait for disconnect. + let _ = read_message::(&mut control_recv).await; + + endpoint.close(0u32.into(), b"done"); + }); + + let config = ClientConfig { + server_addr: addr, + capabilities: vec!["test".to_string()], + }; + + let (_endpoint, mut conn) = connect(&config).await.unwrap(); + assert_eq!(conn.server_hello.session_id, 99); + assert_eq!(conn.server_hello.output_width, 1920); + + // Send a ping so the server's read resolves and it can shut down. + let _ = conn.send_pong(0).await; + + server.await.unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn client_send_input_receive_frame() { + let (endpoint, addr) = start_test_server().await; + + let server = tokio::spawn(async move { + let incoming = endpoint.accept().await.unwrap(); + let connection = incoming.await.unwrap(); + + // Handshake on control stream. + let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap(); + let _: ControlMessage = read_message(&mut control_recv).await.unwrap(); + let server_hello = ControlMessage::ServerHello(ServerHello { + version: wayray_protocol::PROTOCOL_VERSION, + session_id: 1, + output_width: 800, + output_height: 600, + }); + write_message(&mut control_send, &server_hello) + .await + .unwrap(); + + // Send frame on display uni (triggers client's accept_uni). + let mut display_send = connection.open_uni().await.unwrap(); + let frame = DisplayMessage::FrameUpdate(FrameUpdate { + sequence: 7, + regions: vec![], + }); + write_message(&mut display_send, &frame).await.unwrap(); + + // Accept input uni (triggered by client writing input). + let mut input_recv = connection.accept_uni().await.unwrap(); + let input: InputMessage = read_message(&mut input_recv).await.unwrap(); + assert!(matches!(input, InputMessage::Keyboard(_))); + + endpoint.close(0u32.into(), b"done"); + }); + + let config = ClientConfig { + server_addr: addr, + capabilities: vec![], + }; + + let (_endpoint, mut conn) = connect(&config).await.unwrap(); + + // Send input (triggers server's accept_uni for input stream). + let input = InputMessage::Keyboard(KeyboardEvent { + keycode: 42, + state: KeyState::Pressed, + time: 1000, + }); + conn.send_input(&input).await.unwrap(); + + // Accept display stream (triggered by server writing frame). + let mut display_recv = conn.accept_display_stream().await.unwrap(); + let frame = read_display_message(&mut display_recv).await.unwrap(); + let DisplayMessage::FrameUpdate(update) = frame; + assert_eq!(update.sequence, 7); + + server.await.unwrap(); + } +} diff --git a/crates/wrsrvd/Cargo.toml b/crates/wrsrvd/Cargo.toml index ca5ec7e..f31388d 100644 --- a/crates/wrsrvd/Cargo.toml +++ b/crates/wrsrvd/Cargo.toml @@ -10,6 +10,7 @@ tracing.workspace = true tracing-subscriber.workspace = true miette.workspace = true thiserror.workspace = true +serde.workspace = true smithay = { version = "0.7", default-features = false, features = [ "wayland_frontend", @@ -19,3 +20,7 @@ smithay = { version = "0.7", default-features = false, features = [ "backend_winit", ] } ctrlc = "3" +quinn.workspace = true +rustls.workspace = true +rcgen.workspace = true +tokio.workspace = true diff --git a/crates/wrsrvd/src/main.rs b/crates/wrsrvd/src/main.rs index fa3be20..6da893e 100644 --- a/crates/wrsrvd/src/main.rs +++ b/crates/wrsrvd/src/main.rs @@ -1,6 +1,7 @@ mod backend; mod errors; mod handlers; +pub mod network; mod state; use crate::state::WayRay; diff --git a/crates/wrsrvd/src/network.rs b/crates/wrsrvd/src/network.rs new file mode 100644 index 0000000..4bfe5a4 --- /dev/null +++ b/crates/wrsrvd/src/network.rs @@ -0,0 +1,605 @@ +//! QUIC transport server for wrsrvd. +//! +//! Runs a tokio runtime in a background thread, accepting a single client +//! connection. Communicates with the compositor via `std::sync::mpsc` channels. +//! +//! Three logical QUIC streams: +//! - **Control** (bidirectional): handshake, ping/pong, frame acks +//! - **Display** (server→client, unidirectional): frame updates +//! - **Input** (client→server, unidirectional): keyboard/pointer events +//! +//! ## Quinn stream semantics +//! +//! Quinn streams are lazily materialized on the wire: `open_bi()` and +//! `open_uni()` return immediately, but the peer's `accept_bi()`/`accept_uni()` +//! won't resolve until data is actually written to the stream. The handshake +//! protocol accounts for this by having each side write data before the other +//! side tries to accept. + +use std::net::SocketAddr; +use std::sync::mpsc; +use std::thread; + +use quinn::rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use rcgen::CertifiedKey; +use tokio::runtime::Runtime; +use tracing::{error, info, warn}; +use wayray_protocol::codec; +use wayray_protocol::messages::{ + ClientHello, ControlMessage, DisplayMessage, FrameUpdate, InputMessage, ServerHello, +}; + +/// Messages sent from the compositor to the network thread. +pub enum CompositorToNet { + /// Send a frame update to the connected client. + SendFrame(FrameUpdate), + /// Shut down the network thread. + Shutdown, +} + +/// Messages sent from the network thread to the compositor. +pub enum NetToCompositor { + /// An input event received from the client. + Input(InputMessage), + /// A control message (e.g., FrameAck) from the client. + Control(ControlMessage), + /// Client connected with the given hello. + ClientConnected(ClientHello), + /// Client disconnected. + ClientDisconnected, +} + +/// Configuration for the QUIC server. +pub struct ServerConfig { + /// Address to bind to. Defaults to `0.0.0.0:4433`. + pub bind_addr: SocketAddr, + /// Virtual output dimensions for the ServerHello. + pub output_width: u32, + /// Virtual output dimensions for the ServerHello. + pub output_height: u32, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + bind_addr: "0.0.0.0:4433".parse().unwrap(), + output_width: 1280, + output_height: 720, + } + } +} + +/// Handle to a running network server thread. +pub struct NetworkHandle { + /// Send commands to the network thread. + pub tx: mpsc::Sender, + /// Receive events from the network thread. + pub rx: mpsc::Receiver, + /// Join handle for the background thread. + join: Option>, +} + +impl NetworkHandle { + /// Shut down the network thread and wait for it to exit. + pub fn shutdown(mut self) { + let _ = self.tx.send(CompositorToNet::Shutdown); + if let Some(handle) = self.join.take() { + let _ = handle.join(); + } + } +} + +impl Drop for NetworkHandle { + fn drop(&mut self) { + let _ = self.tx.send(CompositorToNet::Shutdown); + if let Some(handle) = self.join.take() { + let _ = handle.join(); + } + } +} + +/// Generate a self-signed TLS certificate for the QUIC server. +fn generate_self_signed_cert() -> (Vec>, PrivateKeyDer<'static>) { + let CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(vec!["localhost".to_string(), "wayray".to_string()]) + .expect("certificate generation failed"); + let cert_der = CertificateDer::from(cert); + let key_der = PrivateKeyDer::try_from(key_pair.serialize_der()).expect("key serialization"); + (vec![cert_der], key_der) +} + +/// Build a quinn `ServerConfig` from a self-signed cert. +fn build_server_config() -> quinn::ServerConfig { + let (certs, key) = generate_self_signed_cert(); + let provider = rustls::crypto::ring::default_provider(); + let crypto = rustls::ServerConfig::builder_with_provider(provider.into()) + .with_safe_default_protocol_versions() + .expect("TLS protocol versions") + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("TLS server config"); + let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(crypto) + .expect("QUIC server crypto config"); + quinn::ServerConfig::with_crypto(std::sync::Arc::new(crypto)) +} + +/// Start the QUIC server on a background thread. +/// +/// Returns a `NetworkHandle` with channels for communication. +/// The server accepts one client at a time. When a client disconnects, +/// it loops back to accept the next one. +pub fn start_server(config: ServerConfig) -> NetworkHandle { + let (comp_tx, net_rx) = mpsc::channel::(); + let (net_tx, comp_rx) = mpsc::channel::(); + + let join = thread::Builder::new() + .name("wayray-net".into()) + .spawn(move || { + let rt = Runtime::new().expect("tokio runtime"); + rt.block_on(async move { + if let Err(e) = server_loop(config, net_rx, net_tx).await { + error!("network server error: {e}"); + } + }); + }) + .expect("spawn network thread"); + + NetworkHandle { + tx: comp_tx, + rx: comp_rx, + join: Some(join), + } +} + +/// Main server accept loop. +async fn server_loop( + config: ServerConfig, + compositor_rx: mpsc::Receiver, + compositor_tx: mpsc::Sender, +) -> Result<(), Box> { + let server_config = build_server_config(); + let endpoint = quinn::Endpoint::server(server_config, config.bind_addr)?; + info!(addr = %config.bind_addr, "QUIC server listening"); + + loop { + // Check for shutdown before waiting for connection. + if let Ok(CompositorToNet::Shutdown) = compositor_rx.try_recv() { + info!("network: shutdown requested"); + break; + } + + let incoming = tokio::select! { + incoming = endpoint.accept() => { + match incoming { + Some(incoming) => incoming, + None => { + info!("QUIC endpoint closed"); + break; + } + } + } + _ = check_shutdown_async(&compositor_rx) => { + info!("network: shutdown during accept"); + break; + } + }; + + let connection = match incoming.await { + Ok(conn) => conn, + Err(e) => { + warn!("failed to accept connection: {e}"); + continue; + } + }; + + info!( + remote = %connection.remote_address(), + "client connected" + ); + + if let Err(e) = + handle_connection(&connection, &config, &compositor_rx, &compositor_tx).await + { + warn!("client session ended: {e}"); + } + + let _ = compositor_tx.send(NetToCompositor::ClientDisconnected); + info!("client disconnected, waiting for next connection"); + } + + endpoint.close(0u32.into(), b"shutdown"); + Ok(()) +} + +/// Poll for shutdown on a blocking channel from an async context. +async fn check_shutdown_async(rx: &mpsc::Receiver) { + loop { + match rx.try_recv() { + Ok(CompositorToNet::Shutdown) => return, + Ok(_) => continue, // drain non-shutdown messages + Err(mpsc::TryRecvError::Disconnected) => return, + Err(mpsc::TryRecvError::Empty) => { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + } +} + +/// Handle a single client connection: handshake on control stream, then +/// relay messages between compositor and client until disconnect. +/// +/// Stream setup protocol (accounts for quinn's lazy stream creation): +/// 1. Client opens bidi stream and immediately sends `ClientHello` (triggers +/// server's `accept_bi`). +/// 2. Server reads `ClientHello`, sends `ServerHello` on control stream. +/// 3. Server opens display uni stream and sends an initial empty frame +/// (triggers client's `accept_uni` for display). +/// 4. Client opens input uni stream — server's `accept_uni` for input is +/// handled asynchronously when the client first writes input data. +async fn handle_connection( + connection: &quinn::Connection, + config: &ServerConfig, + compositor_rx: &mpsc::Receiver, + compositor_tx: &mpsc::Sender, +) -> Result<(), Box> { + // Step 1: Accept control stream. The client writes ClientHello immediately + // after opening, so accept_bi resolves once that data arrives. + let (mut control_send, mut control_recv) = connection.accept_bi().await?; + info!("control stream established"); + + // Step 2: Read ClientHello, send ServerHello. + let client_hello: ControlMessage = read_message(&mut control_recv).await?; + let ControlMessage::ClientHello(hello) = client_hello else { + return Err(format!("expected ClientHello, got {client_hello:?}").into()); + }; + info!(version = hello.version, "received ClientHello"); + let _ = compositor_tx.send(NetToCompositor::ClientConnected(hello)); + + let server_hello = ControlMessage::ServerHello(ServerHello { + version: wayray_protocol::PROTOCOL_VERSION, + session_id: 1, // TODO: real session management + output_width: config.output_width, + output_height: config.output_height, + }); + write_message(&mut control_send, &server_hello).await?; + info!("sent ServerHello"); + + // Step 3: Open display uni stream. Writing data triggers the client's + // accept_uni for this stream. + let mut display_send = connection.open_uni().await?; + info!("display stream opened"); + + // Step 4: Message relay loop. Accept the input uni stream concurrently + // with handling control messages and compositor commands. + let mut input_recv: Option = None; + let mut accepting_input = true; + + loop { + tokio::select! { + // Accept input stream from client (one-shot). + result = connection.accept_uni(), if accepting_input => { + match result { + Ok(recv) => { + info!("input stream established"); + input_recv = Some(recv); + accepting_input = false; + } + Err(e) => { + return Err(format!("failed to accept input stream: {e}").into()); + } + } + } + + // Read control messages from client. + msg = read_message::(&mut control_recv) => { + match msg { + Ok(ctrl) => { + let _ = compositor_tx.send(NetToCompositor::Control(ctrl)); + } + Err(e) => { + return Err(format!("control stream error: {e}").into()); + } + } + } + + // Read input messages from client (only when stream is established). + msg = async { + match input_recv.as_mut() { + Some(recv) => read_message::(recv).await, + None => std::future::pending().await, + } + } => { + match msg { + Ok(input) => { + let _ = compositor_tx.send(NetToCompositor::Input(input)); + } + Err(e) => { + return Err(format!("input stream error: {e}").into()); + } + } + } + + // Check for messages from the compositor. + _ = check_compositor_commands( + compositor_rx, + &mut display_send, + ) => { + // Shutdown requested. + return Ok(()); + } + } + } +} + +/// Process commands from the compositor channel. Returns when shutdown +/// is requested or the channel is disconnected. +async fn check_compositor_commands( + rx: &mpsc::Receiver, + display_send: &mut quinn::SendStream, +) { + loop { + match rx.try_recv() { + Ok(CompositorToNet::SendFrame(frame)) => { + let msg = DisplayMessage::FrameUpdate(frame); + if let Err(e) = write_message(display_send, &msg).await { + warn!("failed to send frame: {e}"); + return; + } + } + Ok(CompositorToNet::Shutdown) => return, + Err(mpsc::TryRecvError::Disconnected) => return, + Err(mpsc::TryRecvError::Empty) => { + // Yield to let other select branches run. + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + } +} + +/// Read a length-prefixed message from a QUIC receive stream. +async fn read_message( + recv: &mut quinn::RecvStream, +) -> Result> { + // Read 4-byte length prefix. + let mut len_buf = [0u8; 4]; + recv.read_exact(&mut len_buf).await?; + let len = u32::from_le_bytes(len_buf) as usize; + + // Read payload. + let mut payload = vec![0u8; len]; + recv.read_exact(&mut payload).await?; + + let msg = codec::decode(&payload)?; + Ok(msg) +} + +/// Write a length-prefixed message to a QUIC send stream. +async fn write_message( + send: &mut quinn::SendStream, + msg: &T, +) -> Result<(), Box> { + let encoded = codec::encode(msg)?; + send.write_all(&encoded).await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use wayray_protocol::messages::ControlMessage; + + /// Build a test client endpoint with certificate verification disabled. + fn build_test_client_endpoint() -> quinn::Endpoint { + let provider = rustls::crypto::ring::default_provider(); + let crypto = rustls::ClientConfig::builder_with_provider(provider.into()) + .with_safe_default_protocol_versions() + .expect("TLS protocol versions") + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(SkipServerVerification)) + .with_no_client_auth(); + let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(crypto).unwrap(); + let client_config = quinn::ClientConfig::new(std::sync::Arc::new(crypto)); + + let mut endpoint = + quinn::Endpoint::client("127.0.0.1:0".parse::().unwrap()).unwrap(); + endpoint.set_default_client_config(client_config); + endpoint + } + + /// Dummy certificate verifier that accepts any server cert. + #[derive(Debug)] + struct SkipServerVerification; + + impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn server_client_hello_exchange() { + let server_config = build_server_config(); + let endpoint = + quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap(); + let actual_addr = endpoint.local_addr().unwrap(); + + // Server side: accept bi, read ClientHello, send ServerHello. + let server_handle = tokio::spawn(async move { + let incoming = endpoint.accept().await.unwrap(); + let connection = incoming.await.unwrap(); + + // Client writes ClientHello immediately, so accept_bi resolves. + let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap(); + + let hello: ControlMessage = read_message(&mut control_recv).await.unwrap(); + let ControlMessage::ClientHello(client_hello) = hello else { + panic!("expected ClientHello"); + }; + assert_eq!(client_hello.version, wayray_protocol::PROTOCOL_VERSION); + + let server_hello = ControlMessage::ServerHello(ServerHello { + version: wayray_protocol::PROTOCOL_VERSION, + session_id: 42, + output_width: 1920, + output_height: 1080, + }); + write_message(&mut control_send, &server_hello) + .await + .unwrap(); + + // Wait for client to read the response before closing. + let _ = read_message::(&mut control_recv).await; + endpoint.close(0u32.into(), b"done"); + }); + + // Client side: open bi and immediately send ClientHello. + let client_endpoint = build_test_client_endpoint(); + let connection = client_endpoint + .connect(actual_addr, "localhost") + .unwrap() + .await + .unwrap(); + + let (mut control_send, mut control_recv) = connection.open_bi().await.unwrap(); + + // Write immediately to trigger server's accept_bi. + let client_hello = ControlMessage::ClientHello(ClientHello { + version: wayray_protocol::PROTOCOL_VERSION, + capabilities: vec!["display".to_string()], + }); + write_message(&mut control_send, &client_hello) + .await + .unwrap(); + + // Read ServerHello. + let response: ControlMessage = read_message(&mut control_recv).await.unwrap(); + let ControlMessage::ServerHello(server_hello) = response else { + panic!("expected ServerHello, got {response:?}"); + }; + assert_eq!(server_hello.version, wayray_protocol::PROTOCOL_VERSION); + assert_eq!(server_hello.session_id, 42); + assert_eq!(server_hello.output_width, 1920); + assert_eq!(server_hello.output_height, 1080); + + // Signal server to close. + let ping = ControlMessage::Ping(wayray_protocol::messages::Ping { timestamp: 0 }); + let _ = write_message(&mut control_send, &ping).await; + + server_handle.await.unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn server_display_and_input_streams() { + let server_config = build_server_config(); + let endpoint = + quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap(); + let actual_addr = endpoint.local_addr().unwrap(); + + let server_handle = tokio::spawn(async move { + let incoming = endpoint.accept().await.unwrap(); + let connection = incoming.await.unwrap(); + + // Accept control and do handshake. + let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap(); + let _hello: ControlMessage = read_message(&mut control_recv).await.unwrap(); + let server_hello = ControlMessage::ServerHello(ServerHello { + version: wayray_protocol::PROTOCOL_VERSION, + session_id: 1, + output_width: 1280, + output_height: 720, + }); + write_message(&mut control_send, &server_hello) + .await + .unwrap(); + + // Open display uni and send a frame (triggers client's accept_uni). + let mut display_send = connection.open_uni().await.unwrap(); + let frame = DisplayMessage::FrameUpdate(FrameUpdate { + sequence: 1, + regions: vec![], + }); + write_message(&mut display_send, &frame).await.unwrap(); + + // Accept input uni (triggered by client writing input). + let mut input_recv = connection.accept_uni().await.unwrap(); + let input: InputMessage = read_message(&mut input_recv).await.unwrap(); + assert!(matches!(input, InputMessage::Keyboard(_))); + + endpoint.close(0u32.into(), b"done"); + }); + + // Client side. + let client_endpoint = build_test_client_endpoint(); + let connection = client_endpoint + .connect(actual_addr, "localhost") + .unwrap() + .await + .unwrap(); + + // Open control and send ClientHello. + let (mut control_send, mut control_recv) = connection.open_bi().await.unwrap(); + let client_hello = ControlMessage::ClientHello(ClientHello { + version: wayray_protocol::PROTOCOL_VERSION, + capabilities: vec![], + }); + write_message(&mut control_send, &client_hello) + .await + .unwrap(); + let _: ControlMessage = read_message(&mut control_recv).await.unwrap(); + + // Open input uni and send keyboard event (triggers server's accept_uni). + let mut input_send = connection.open_uni().await.unwrap(); + let input = InputMessage::Keyboard(wayray_protocol::messages::KeyboardEvent { + keycode: 42, + state: wayray_protocol::messages::KeyState::Pressed, + time: 1000, + }); + write_message(&mut input_send, &input).await.unwrap(); + + // Accept display uni from server (triggered by server writing frame). + let mut display_recv = connection.accept_uni().await.unwrap(); + let frame: DisplayMessage = read_message(&mut display_recv).await.unwrap(); + let DisplayMessage::FrameUpdate(update) = frame; + assert_eq!(update.sequence, 1); + assert!(update.regions.is_empty()); + + server_handle.await.unwrap(); + } + + #[test] + fn cert_generation_works() { + let (certs, _key) = generate_self_signed_cert(); + assert_eq!(certs.len(), 1); + } +}