diff --git a/Cargo.lock b/Cargo.lock index 107a957..4ab0a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,46 +3,22 @@ version = 4 [[package]] -name = "addr2line" -version = "0.24.2" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "adler2" -version = "2.0.0" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -55,15 +31,41 @@ dependencies = [ [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -76,9 +78,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -86,9 +88,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "digest" @@ -100,6 +102,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" @@ -107,16 +121,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "futures-core" -version = "0.3.31" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -125,28 +157,27 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", "futures-sink", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] @@ -162,33 +193,109 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -196,56 +303,145 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "itoa" -version = "1.0.15" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "miniz_oxide" -version = "0.8.8" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ - "adler2", + "mime", + "unicase", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ - "memchr", + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -289,15 +485,15 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -315,18 +511,170 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +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 = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] [[package]] name = "sha1" @@ -340,24 +688,39 @@ dependencies = [ ] [[package]] -name = "slab" -version = "0.4.9" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -371,18 +734,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -391,25 +754,24 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -417,22 +779,87 @@ dependencies = [ ] [[package]] -name = "tokio-tungstenite" -version = "0.26.2" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.27.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite 0.29.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", ] [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", @@ -446,10 +873,34 @@ dependencies = [ ] [[package]] -name = "typenum" -version = "1.18.0" +name = "tungstenite" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -457,6 +908,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf-8" version = "0.7.6" @@ -470,20 +927,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "warp" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tungstenite 0.27.0", + "tokio-util", + "tower-service", + "tracing", +] [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -495,11 +988,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -567,39 +1060,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] -name = "wstcp" +name = "wsudp" version = "0.1.0" dependencies = [ "futures-util", + "rustls", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", + "warp", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 37a1253..85b86ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,11 @@ [package] -name = "wstcp" +name = "wsudp" version = "0.1.0" edition = "2024" [dependencies] -futures-util = "0.3.31" -tokio = { version = "1.45.1", features = ["macros", "net", "rt", "time"] } -tokio-tungstenite = "0.26.2" +futures-util = "0.3.32" +rustls = { version = "0.23.38", default-features = false, features = ["ring"] } +tokio = { version = "1.52.0", features = ["macros", "net", "rt", "time"] } +tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } +warp = { version = "0.4.2", features = ["websocket", "server"] } diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..3fc051a --- /dev/null +++ b/src/args.rs @@ -0,0 +1,149 @@ +use crate::routes::Prot; + +pub struct Args { + pub verbosity: u8, + pub binds: Vec, +} + +pub struct Ref { + pub spec: String, +} +pub struct Bind { + pub spec: String, + pub id: Option<(Prot, usize)>, + pub prot: String, + pub arg: String, + pub wires: Vec>, +} +pub struct Wire { + pub spec: String, + pub prot: String, + pub arg: String, +} + +pub fn args() -> Result { + let mut binds = Vec::new(); + let mut verbosity = 0u8; + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + #[allow(irrefutable_let_patterns)] + if let arg = arg.trim_start() + && arg.starts_with('-') + { + match arg { + "-h" | "--help" => { + return Err(" +Arguments: [option1] [option2] [...] [bind1] [wire0] [wire1] [...] [bind2] [wire3] [...] [...] + + +Options: + +`-h, --help` Print this text and exit. +`-v, --verbose` Output more stuff. Can be used multiple times. +`--verbosity ` Same as specifying `-v` n times. + + +Binds and Wires (non-option arguments): + +Binds are specified as `@` or `=@`. +Wires can be ``, `:` or `=:`. +Wires belong to the last bind before the wire. Just `` means reusing another bind or wire. +For each bind, `wsudp` will accept (but not receive data from) connections. +Data received on a connection will be sent to its wires if at least one is present. +Data received from the first wire, if present, will be sent to the connection. + + +Protocols: + +`tcp` - bind/wire for TCP. +note: accepts addresses as `:`, `[]:` or `:`. +bind: binds to the given address (e.g. `[::1]:8001`) and accepts connections. +wire: connects to the given address if a process is accepting connections on it. + +`udp` - bind/wire for UDP. +note: accepts addresses as `:`, `[]:`, or `:`. + unlike most protocols here, udp may drop packets. non-udp programs generally + don't handle this well and may fail in some way when it happens. + tcp->udp->tcp can break things, udp->tcp->udp works but is slower. +bind: binds to the given address (e.g. `[::1]:8001`) and receives packets. + since UDP doesn't have connections, messages from the same source address + are considered being from the same connection. if a source is idle for + too long and then sends messages again, this may be considered a new connection. +wire: sends/receives messages to/from the specified address. + +`ws` - bind/wire for websocket and wire for wss +note: accepts `:`, `[]:`, optionally with a `/` suffix. +bind: binds to the given address and accepts websocket connections on any path. +wire: connects to the given address and path. prefix the address with `wss://` to use tls. + bind doesn't (yet) support dns lookups, but wire does. default ports are 80/443. + + +Examples: + +Start a websocket server and connect clients to a udp socket: +`wsudp ws@[::1]:8001 udp:[::1]:8002` + +Start a tcp server and connect clients to a websocket server, +while also sending client messages to a udp socket: +`wsudp tcp@[::1]:8001 ws:[::1]:80/api udp:127.0.0.1:8002` + +Start a tcp and udp server and connect clients to a websocket server: +`wsudp tcp@[::1]:8001 web udp@[::1]:8002 web=ws:localhost:8000/socket` + +" + .to_owned()); + } + "-v" | "--verbose" => verbosity += 1, + "--verbosity" => { + verbosity = verbosity.saturating_add( + args.next() + .expect("Missing arguments after `--verbosity`") + .trim() + .parse() + .expect("Invalid data specified for `--verbosity`"), + ) + } + _ => return Err(format!("Unknown argument: `{arg}`")), + } + } else if let Some(i) = arg.find([':', '@']) { + let is_bind = &arg[i..=i] == "@"; + if binds.is_empty() && !is_bind { + return Err(format!( + "Found `:` instead of `@` when specifying a bind: `{arg}`" + )); + } + let (spec, prot) = if let Some(j) = arg[..i].rfind('=') { + (&arg[..j], &arg[j + 1..i]) + } else { + ("", &arg[..i]) + }; + let arg = &arg[i + 1..]; + if is_bind { + binds.push(Bind { + spec: spec.to_owned(), + id: None, + prot: prot.to_owned(), + arg: arg.to_owned(), + wires: Vec::new(), + }); + } else { + binds + .last_mut() + .expect("there's a check to prevent this") + .wires + .push(Ok(Wire { + spec: spec.to_owned(), + prot: prot.to_owned(), + arg: arg.to_owned(), + })); + } + } else if let Some(bind) = binds.last_mut() { + bind.wires.push(Err(Ref { + spec: arg.to_owned(), + })); + } else { + return Err(format!("Found no `@` when specifying a bind: `{arg}`")); + } + } + Ok(Args { verbosity, binds }) +} diff --git a/src/main.rs b/src/main.rs index e3153b0..6dc93dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,178 @@ -use std::time::Duration; +#![allow(dead_code)] +mod args; +mod routes; +mod tcp; +mod udp; +mod ws; -use futures_util::{SinkExt, StreamExt}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::{TcpListener, TcpStream}, - spawn, - time::sleep, -}; -use tokio_tungstenite::tungstenite::Message; +use std::{collections::HashMap, process::ExitCode}; + +use crate::routes::Routes; + +// can contain most udp packets +const BUF_SIZE: usize = 2048; + +// TODO: +// +// - websocket: request path (prefix) matching, +// - websocket: reuse one server for multiple routes +// - udp, ... + +trait Wire: Sized { + type R: Rx; + type T: Tx; + async fn parse(arg: &str) -> Result; + /// Open a new connection. For most protocols, this assumes + /// that some endpoint exists to which we can try to connect. + /// (Unless the implementor also implements `Bind`, for this, + /// read the following sections and the doc of `Bind::wait()`) + /// + /// For `Bind` implementors, this either has priority over `Bind::wait()` or must return `Err(_)`. + /// NOTE: The order in which `Wire::open()` and `Bind::wait()` are called + /// is not known, as this depends on runtime circumstances. + async fn open(&self) -> Result<(Self::R, Self::T), String>; +} +trait Bind: Wire { + type R: Rx; + type T: Tx; + /// This will be called repeatedly to accept incoming connections. + /// + /// The implementor may simply return `Err(_)` from `Wire::open()`. + /// If not, the implementor must support the following: + /// If `Wire::open()` is called `n` times after `Bind::wait()`, + /// the next `n` incoming connections (in order) should be returned + /// from the calls to `Wire::open()` instead of `Bind::wait()` + async fn wait(&self) -> Result<(::R, ::T), String>; +} +trait Rx { + type T<'a>: RxT; + async fn rx<'a>(&mut self, buf: &'a mut [u8]) -> Result, String>; +} +trait Tx { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String>; +} #[tokio::main(flavor = "current_thread")] -async fn main() { - let addr = std::env::var("TCPWS_ADDR").expect("expected env var TCPWS_ADDR"); - let listener = - TcpListener::bind(std::env::var("TCPWS_BIND").expect("expected env var TCPWS_BIND")) - .await - .unwrap(); - loop { - if let Ok((ws, _)) = listener.accept().await { - if let Ok(con) = TcpStream::connect(&addr).await { - spawn(async move { - let (mut con_rx, mut con_tx) = con.into_split(); - let ws = tokio_tungstenite::accept_async(ws).await.unwrap(); - let (mut ws_tx, mut ws_rx) = ws.split(); - spawn(async move { - loop { - match ws_rx.next().await { - Some(Ok(Message::Binary(data))) => { - con_tx.write_all(&data).await.unwrap(); - } - Some(Ok(Message::Text(data))) => { - con_tx.write_all(data.as_bytes()).await.unwrap(); - } - Some(Ok( - Message::Ping(_) | Message::Pong(_) | Message::Frame(_), - )) => {} - None | Some(Err(_)) | Some(Ok(Message::Close(_))) => { - con_tx.shutdown().await.ok(); - break; - } - } - } - }); - let mut buf = [0u8; 256]; - loop { - match con_rx.read(&mut buf).await { - Err(_) | Ok(0) => { - ws_tx.close().await.ok(); - break; - } - Ok(n) => ws_tx - .send(Message::binary(buf[0..n].to_vec())) - .await - .unwrap(), - } - } - }); +async fn main() -> ExitCode { + let mut args = match args::args() { + Ok(args) => args, + Err(msg) => { + eprintln!("{msg}"); + return ExitCode::FAILURE; + } + }; + + let mut routes = Routes::new(); + routes.config.verbosity = args.verbosity; + + if routes.config.verbosity >= 2 { + eprintln!("[main] preparing..."); + } + + let mut specs = HashMap::new(); + for bind in &mut args.binds { + match routes.bind(&bind.prot, &bind.arg).await { + Ok(id) => bind.id = Some(id), + Err(msg) => { + eprintln!("{msg}"); + return ExitCode::FAILURE; } - } else { - sleep(Duration::from_millis(100)).await; } } + for bind in &args.binds { + if !bind.spec.is_empty() && specs.insert(bind.spec.as_str(), bind.id.unwrap()).is_some() { + eprintln!("Duplicate definition for `{}`", bind.spec); + return ExitCode::FAILURE; + } + } + for (bind, wire) in args.binds.iter().flat_map(|bind| { + let id = bind.id.expect("set in a previous loop"); + bind.wires + .iter() + .filter_map(|w| w.as_ref().ok()) + .map(move |w| (id, w)) + }) { + match routes.wire(&wire.prot, &wire.arg).await { + Ok(id) => { + if !wire.spec.is_empty() && specs.insert(wire.spec.as_str(), id).is_some() { + eprintln!("Duplicate definition for `{}`", wire.spec); + return ExitCode::FAILURE; + } + match routes.pipe(bind, id) { + Ok(()) => {} + Err(msg) => { + eprintln!("{msg}"); + return ExitCode::FAILURE; + } + } + } + Err(msg) => { + eprintln!("{msg}"); + return ExitCode::FAILURE; + } + } + } + for (bind, wire) in args.binds.iter().flat_map(|bind| { + let id = bind.id.expect("set in a previous loop"); + bind.wires + .iter() + .filter_map(|w| w.as_ref().err()) + .map(move |w| (id, w)) + }) { + if let Some(wire) = specs.get(&wire.spec.as_str()) { + match routes.pipe(bind, *wire) { + Ok(()) => {} + Err(msg) => { + eprintln!("{msg}"); + return ExitCode::FAILURE; + } + } + } else { + if wire.spec.is_empty() { + eprintln!("Empty string `` is not a valid name"); + } else { + eprintln!("No definition for `{}`", wire.spec); + } + return ExitCode::FAILURE; + } + } + + if routes.config.verbosity >= 1 { + eprintln!("[main] running."); + } + + for task in routes.execute().await { + if let Err(e) = task.await { + eprintln!("[ERR] {e}"); + } + } + + eprintln!("[main] goodbye"); + + ExitCode::SUCCESS +} + +trait RxT { + fn buf(&self) -> &[u8]; +} +impl RxT for Vec { + fn buf(&self) -> &[u8] { + &self[..] + } +} +impl RxT for &[u8] { + fn buf(&self) -> &[u8] { + self + } +} +impl Rx for &mut T { + type T<'a> = T::T<'a>; + async fn rx<'a>(&mut self, buf: &'a mut [u8]) -> Result, String> { + (*self).rx(buf).await + } +} +impl Tx for &mut T { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + (*self).tx(buf).await + } } diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..534ed62 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,332 @@ +use std::sync::Arc; +use tokio::task::spawn; + +#[derive(Clone, Copy, Default)] +pub struct Config { + pub verbosity: u8, +} + +#[derive(Default)] +pub struct Routes { + pub config: Config, + binds: Binds, + wires: Wires, + state: State, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Prot(ProtType, bool); +macro_rules! protocols { + ($($p:ident:$t:ident),*) => { + #[derive(Clone, Copy, PartialEq, Eq)] + pub enum ProtType { + $($t,)* + } + pub enum ProtRx { + $($t($crate::$p::Rx),)* + } + pub enum ProtTx { + $($t($crate::$p::Tx),)* + } + #[derive(Default)] + struct Binds { + $($p: Bind<$crate::$p::Bind>,)* + } + #[derive(Default)] + struct Wires { + $($p: Wire<$crate::$p::Wire>,)* + } + #[derive(Default)] + struct State { + $($p: $crate::$p::State,)* + } + use $crate::{Rx, Tx}; + impl Tx for ProtTx { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + match self { + $( + ProtTx::$t($crate::$p::Tx::Wait(tx)) => tx.tx(buf).await, + ProtTx::$t($crate::$p::Tx::Bind(tx)) => tx.tx(buf).await, + ProtTx::$t($crate::$p::Tx::Wire(tx)) => tx.tx(buf).await, + )* + } + } + } + async fn prot_fwd(r: &mut ProtRx, tx: &mut impl Tx) -> Result { + use $crate::{BUF_SIZE, RxT}; + match r { + $( + ProtRx::$t($crate::$p::Rx::Wait(r)) => { + let mut buf = [0u8; BUF_SIZE]; + let buf = r.rx(&mut buf).await?; + let buf = buf.buf(); + if buf.is_empty() { + return Ok(false); + } + tx.tx(buf).await?; + } + ProtRx::$t($crate::$p::Rx::Bind(r)) => { + let mut buf = [0u8; BUF_SIZE]; + let buf = r.rx(&mut buf).await?; + let buf = buf.buf(); + if buf.is_empty() { + return Ok(false); + } + tx.tx(buf).await?; + } + ProtRx::$t($crate::$p::Rx::Wire(r)) => { + let mut buf = [0u8; BUF_SIZE]; + let buf = r.rx(&mut buf).await?; + let buf = buf.buf(); + if buf.is_empty() { + return Ok(false); + } + tx.tx(buf).await?; + } + ,)* + } + Ok(true) + } + async fn prot_drop(r: &mut ProtRx) -> Result { + use $crate::{BUF_SIZE, RxT}; + match r { + $( + ProtRx::$t($crate::$p::Rx::Wait(r)) => { + r.rx(&mut [0u8; BUF_SIZE]).await.map(|buf| !buf.buf().is_empty()) + } + ProtRx::$t($crate::$p::Rx::Bind(r)) => { + r.rx(&mut [0u8; BUF_SIZE]).await.map(|buf| !buf.buf().is_empty()) + } + ProtRx::$t($crate::$p::Rx::Wire(r)) => { + r.rx(&mut [0u8; BUF_SIZE]).await.map(|buf| !buf.buf().is_empty()) + } + )* + } + } + impl Routes { + pub fn has(&self, id: (Prot, usize)) -> bool { + match (id.0.wire(), id.0.prot()) { + $((true, ProtType::$t) => self.wires.$p.get(id.1).is_some(), + (false, ProtType::$t) => self.binds.$p.get(id.1).is_some(),)* + } + } + pub async fn bind( + &mut self, + prot: impl AsRef, + arg: impl AsRef, + ) -> Result<(Prot, usize), String> { + use $crate::Wire; + match prot.as_ref().to_lowercase().as_str() { + $(stringify!($p) => { + let bind = crate::$p::Bind::parse(arg.as_ref()).await?; + Ok((ProtType::$t.bind(), self.binds.$p.add(bind))) + },)* + _ => return Err(format!("unknown/unsupported protocol: `{}`", prot.as_ref())), + } + } + pub async fn wire( + &mut self, + prot: impl AsRef, + arg: impl AsRef, + ) -> Result<(Prot, usize), String> { + use $crate::Wire; + match prot.as_ref().to_lowercase().as_str() { + $(stringify!($p) => { + let wire = crate::$p::Wire::parse(arg.as_ref()).await?; + Ok((ProtType::$t.wire(), self.wires.$p.add(wire))) + },)* + _ => return Err(format!("unknown/unsupported protocol: `{}`", prot.as_ref())), + } + } + pub fn pipe(&mut self, bind: (Prot, usize), wire: (Prot, usize)) -> Result<(), String> { + if !bind.0.bind() { + Err("can't pipe data from a wire (:), you need to use a bind (@).")?; + } + if !self.has(wire) { + Err("internal error: bad wire index")?; + } + match bind.0.prot() { + $(ProtType::$t => self.binds + .$p + .pipe(bind.1, wire) + .ok_or("internal error: bad bind index")?,)* + } + Ok(()) + } + pub async fn execute(self) -> Vec> { + use $crate::{Bind, Rx, Tx, RxT, BUF_SIZE}; + let mut tasks = Vec::new(); + let config = self.config; + let routes = Arc::new(self); + $({ + for i in 0..routes.binds.$p.0.len() { + let routes = Arc::clone(&routes); + tasks.push(spawn(async move { + let (bind, _) = &routes.binds.$p.0[i]; + while let Ok((mut rx, mut tx)) = bind.wait().await { + if config.verbosity >= 2 { + eprintln!("[binds] new connection."); + } + let routes = Arc::clone(&routes); + spawn(async move { + let (_, wires) = &routes.binds.$p.0[i]; + // connect all wires + let mut rxs = Vec::new(); + let mut txs = Vec::new(); + if config.verbosity >= 3 { + eprintln!("[wires] connecting..."); + } + for (j, wire) in wires.iter().enumerate() { + match routes.wire_open(*wire).await { + Ok((r, t)) => { + rxs.push(r); + txs.push(t); + } + Err(e) => if config.verbosity >= if j == 0 { 1 } else { 3 } { + eprintln!("[wires] failed to connect: {e}"); + } + } + } + if config.verbosity >= 2 { + eprintln!("[wires] connected to {}/{} wires.", txs.len(), wires.len()); + } + // forward data to all wires + spawn(async move { + let mut buf = [0u8; BUF_SIZE]; + let mut any_ok = 1; + while any_ok > 0 { + any_ok = 0; + let buf = rx.rx(&mut buf).await?; + let buf = buf.buf(); + if buf.is_empty() { + break; + } + for t in &mut txs { + if t.tx(buf).await.is_ok() { + any_ok += 1; + } + } + if config.verbosity >= 4 { + eprintln!("[wires] forwarded {} bytes through {}/{} wires.", buf.len(), any_ok, txs.len()); + } + } + Ok::<(), String>(()) + }); + // get the first wire + rxs.reverse(); + let r = rxs.pop(); + // drop data from other wires + spawn(async move { + let mut any_ok = true; + while any_ok { + any_ok = false; + for r in &mut rxs { + if let Ok(true) = prot_drop(r).await { + any_ok = true; + } + } + } + }); + // forward data from first wire + if let Some(mut r) = r { + while let Ok(true) = prot_fwd(&mut r, &mut tx).await { + if config.verbosity >= 4 { + eprintln!("[wires] forwarded response."); + } + } + } + }); + } + })); + } + })* + tasks + } + async fn wire_open(&self, id: (Prot, usize)) -> Result<(ProtRx, ProtTx), String> { + use $crate::Wire; + Ok(match (id.0.wire(), id.0.prot()) { + $((true, ProtType::$t) => { + let (r, t) = self.wires.$p.get(id.1).unwrap().open().await?; + (ProtRx::$t($crate::$p::Rx::Wire(r)), ProtTx::$t($crate::$p::Tx::Wire(t))) + } + (false, ProtType::$t) => { + let (r, t) = self.binds.$p.get(id.1).unwrap().open().await?; + (ProtRx::$t($crate::$p::Rx::Bind(r)), ProtTx::$t($crate::$p::Tx::Bind(t))) + } + )*}) + } + } + }; +} + +protocols!(tcp:Tcp, udp:Udp, ws:Ws); + +struct Bind(Vec<(T, Vec<(Prot, usize)>)>); +struct Wire(Vec<(T,)>); + +impl Routes { + pub fn new() -> Self { + Self::default() + } +} + +impl Bind { + fn get(&self, id: usize) -> Option<&T> { + self.0.get(id).map(|(t, _)| t) + } + fn get_mut(&mut self, id: usize) -> Option<&mut T> { + self.0.get_mut(id).map(|(t, _)| t) + } + fn add(&mut self, bind: T) -> usize { + self.0.push((bind, Vec::new())); + self.0.len() - 1 + } + fn pipe(&mut self, id: usize, wire: (Prot, usize)) -> Option<()> { + let (_, pipes) = self.0.get_mut(id)?; + pipes.push(wire); + Some(()) + } +} +impl Wire { + fn get(&self, id: usize) -> Option<&T> { + self.0.get(id).map(|(t,)| t) + } + fn get_mut(&mut self, id: usize) -> Option<&mut T> { + self.0.get_mut(id).map(|(t,)| t) + } + fn add(&mut self, wire: T) -> usize { + self.0.push((wire,)); + self.0.len() - 1 + } +} + +impl ProtType { + fn bind(self) -> Prot { + Prot(self, false) + } + fn wire(self) -> Prot { + Prot(self, true) + } +} +impl Prot { + fn prot(&self) -> ProtType { + self.0 + } + fn bind(&self) -> bool { + !self.1 + } + fn wire(&self) -> bool { + self.1 + } +} + +impl Default for Bind { + fn default() -> Self { + Self(Vec::new()) + } +} +impl Default for Wire { + fn default() -> Self { + Self(Vec::new()) + } +} diff --git a/src/tcp.rs b/src/tcp.rs new file mode 100644 index 0000000..77910de --- /dev/null +++ b/src/tcp.rs @@ -0,0 +1,89 @@ +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{ + TcpListener, TcpStream, + tcp::{OwnedReadHalf, OwnedWriteHalf}, + }, +}; + +#[derive(Default)] +pub struct State; + +pub struct Bind(TcpListener); + +pub struct Wire(String); + +pub enum Rx { + Wait(Rx1), + Bind(Rx1), + Wire(Rx1), +} +pub enum Tx { + Wait(Tx1), + Bind(Tx1), + Wire(Tx1), +} +pub struct Rx1(OwnedReadHalf); +pub struct Tx1(OwnedWriteHalf); + +impl crate::Bind for Bind { + type R = Rx1; + type T = Tx1; + async fn wait(&self) -> Result<(::R, ::T), String> { + let (con, _) = self + .0 + .accept() + .await + .map_err(|e| format!("[tcp] accepting a new connection failed: {e}"))?; + let (rx, tx) = con.into_split(); + Ok((Rx1(rx), Tx1(tx))) + } +} +impl crate::Wire for Bind { + type R = Rx1; + type T = Tx1; + async fn parse(arg: &str) -> Result { + let addr = arg; + Ok(Self(TcpListener::bind(addr).await.map_err(|e| { + format!("[tcp] binding to `{addr}` failed: {e}") + })?)) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + Err("using binds as wires is not (yet) supported for tcp".to_owned()) + } +} +impl crate::Wire for Wire { + type R = Rx1; + type T = Tx1; + async fn parse(arg: &str) -> Result { + Ok(Self(arg.to_owned())) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + let (rx, tx) = TcpStream::connect(&self.0) + .await + .map_err(|e| format!("[tcp] connecting to `{}` failed: {e}", &self.0))? + .into_split(); + Ok((Rx1(rx), Tx1(tx))) + } +} + +impl crate::Rx for Rx1 { + type T<'a> = &'a [u8]; + + async fn rx<'a>(&mut self, buf: &'a mut [u8]) -> Result, String> { + let i = self + .0 + .read(buf) + .await + .map_err(|e| format!("[tcp] reading bytes failed: {e}"))?; + Ok(&buf[..i]) + } +} +impl crate::Tx for Tx1 { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + self.0 + .write_all(buf) + .await + .map_err(|e| format!("[tcp] writing bytes failed: {e}")) + } +} diff --git a/src/udp.rs b/src/udp.rs new file mode 100644 index 0000000..cba75d3 --- /dev/null +++ b/src/udp.rs @@ -0,0 +1,192 @@ +use std::{ + collections::{HashMap, VecDeque}, + net::SocketAddr, + sync::Arc, +}; + +use tokio::{ + net::UdpSocket, + spawn, + sync::{ + Mutex, Notify, + mpsc::{Receiver, Sender}, + }, + time::Instant, +}; + +use crate::BUF_SIZE; + +#[derive(Default)] +pub struct State; + +pub struct Bind( + Arc, + #[allow(clippy::type_complexity)] + Arc< + Mutex<( + VecDeque<(SocketAddr, Receiver>)>, + HashMap>, u8, Instant)>, + )>, + >, + Arc, +); + +pub struct Wire(String); + +pub enum Rx { + Wait(Rx1), + Bind(Rx3), + Wire(Rx3), +} +pub enum Tx { + Wait(Tx1), + Bind(Tx3), + Wire(Tx3), +} +pub struct Rx1(Receiver>); +pub struct Tx1(Arc, SocketAddr); +pub struct Rx3(Arc); +pub struct Tx3(Arc); + +impl crate::Bind for Bind { + type R = Rx1; + type T = Tx1; + async fn wait(&self) -> Result<(::R, ::T), String> { + let (addr, recv) = loop { + self.2.notified().await; + let mut lock = self.1.lock().await; + if let Some((addr, recv)) = lock.0.pop_front() { + if !lock.0.is_empty() { + self.2.notify_one(); + } + break (addr, recv); + } + }; + Ok((Rx1(recv), Tx1(Arc::clone(&self.0), addr))) + } +} +impl crate::Wire for Bind { + type R = Rx3; + type T = Tx3; + async fn parse(arg: &str) -> Result { + let addr = arg; + let socket = Arc::new( + UdpSocket::bind(addr) + .await + .map_err(|e| format!("[udp] binding socket to `{addr}` failed: {e}"))?, + ); + let data = Arc::new(Mutex::new(Default::default())); + let notify = Arc::new(Notify::new()); + let out = Self(Arc::clone(&socket), Arc::clone(&data), Arc::clone(¬ify)); + spawn(async move { + let mut i = 0u32; + loop { + i += 1; + let cleanup = i > 10000; + if cleanup { + i = 0; + } + let mut buf = [0u8; BUF_SIZE]; + if let Ok((i, addr)) = socket.recv_from(&mut buf).await { + let mut lock = data.lock().await; + let (queue, map) = &mut *lock; + let mut now = None; + if cleanup { + let now = *now.get_or_insert_with(Instant::now); + map.retain(|_, (_, _, last_active)| (now - *last_active).as_secs() < 1000); + } + map.entry(addr) + .and_modify(|(send, counter, time)| { + if send.try_send(Vec::from(&buf[..i])).is_ok() { + *counter += 1; + if cleanup || *counter > 100 { + *counter = 0; + *time = *now.get_or_insert_with(Instant::now); + } + } + }) + .or_insert_with(|| { + let (send, recv) = tokio::sync::mpsc::channel(4096); + queue.push_back((addr, recv)); + notify.notify_one(); + send.try_send(Vec::from(&buf[..i])).ok(); + (send, 0, Instant::now()) + }); + } else { + break; + }; + } + }); + Ok(out) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + Err("using binds as wires is not (yet) supported for udp".to_owned()) + } +} +impl crate::Wire for Wire { + type R = Rx3; + type T = Tx3; + async fn parse(arg: &str) -> Result { + Ok(Self(arg.to_owned())) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + let socket = UdpSocket::bind("[::]:0") + .await + .map_err(|e| format!("[udp] binding wire failed: {e}"))?; + socket + .connect(&self.0) + .await + .map_err(|e| format!("[udp] connecting to `{}` failed: {e}", &self.0))?; + let socket = Arc::new(socket); + Ok((Rx3(Arc::clone(&socket)), Tx3(socket))) + } +} + +impl crate::Rx for Rx1 { + type T<'a> = Vec; + + async fn rx<'a>(&mut self, _buf: &'a mut [u8]) -> Result, String> { + self.0 + .recv() + .await + .ok_or_else(|| "[udp] receiving message failed, channel closed".to_owned()) + } +} +impl crate::Tx for Tx1 { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + let mut n = 0; + while n < buf.len() { + n += self + .0 + .send_to(&buf[n..], &self.1) + .await + .map_err(|e| format!("[udp] sending message failed: {e}"))?; + } + Ok(()) + } +} +impl crate::Rx for Rx3 { + type T<'a> = &'a [u8]; + + async fn rx<'a>(&mut self, buf: &'a mut [u8]) -> Result, String> { + let i = self + .0 + .recv(buf) + .await + .map_err(|e| format!("[udp] receiving message failed: {e}"))?; + Ok(&buf[..i]) + } +} +impl crate::Tx for Tx3 { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + let mut n = 0; + while n < buf.len() { + n += self + .0 + .send(&buf[n..]) + .await + .map_err(|e| format!("[udp] sending message failed: {e}"))?; + } + Ok(()) + } +} diff --git a/src/ws.rs b/src/ws.rs new file mode 100644 index 0000000..d60a433 --- /dev/null +++ b/src/ws.rs @@ -0,0 +1,151 @@ +use std::net::SocketAddr; + +use futures_util::{ + SinkExt, StreamExt, + stream::{SplitSink, SplitStream}, +}; +use tokio::{ + net::TcpStream, + spawn, + sync::{Mutex, mpsc::Receiver}, +}; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::Message}; +use warp::{ + Filter, + filters::ws::{Message as WsMessage, WebSocket}, +}; + +#[derive(Default)] +pub struct State; + +pub struct Bind(Mutex>); + +pub struct Wire(String); + +pub enum Rx { + Wait(Rx1), + Bind(Rx1), + Wire(Rx3), +} +pub enum Tx { + Wait(Tx1), + Bind(Tx1), + Wire(Tx3), +} +pub struct Rx1(SplitStream); +pub struct Tx1(SplitSink); +pub struct Rx3(SplitStream>>); +pub struct Tx3(SplitSink>, Message>); + +impl crate::Bind for Bind { + type R = Rx1; + type T = Tx1; + async fn wait(&self) -> Result<(::R, ::T), String> { + let con = self + .0 + .lock() + .await + .recv() + .await + .ok_or("[ws] no new connections (sender gone)")?; + let (tx, rx) = con.split(); + Ok((Rx1(rx), Tx1(tx))) + } +} +impl crate::Wire for Bind { + type R = Rx1; + type T = Tx1; + async fn parse(arg: &str) -> Result { + let (addr, _path) = arg.split_once('/').unwrap_or((arg, "")); + let (send, recv) = tokio::sync::mpsc::channel(32); + let routes = warp::ws().map(move |ws: warp::ws::Ws| { + let send = send.clone(); + ws.on_upgrade(async move |websocket| { + send.send(websocket).await.ok(); + }) + }); + let addr = addr + .parse::() + .map_err(|e| format!("[ws] couldn't parse address `{addr}`: {e}"))?; + spawn(warp::serve(routes).run(addr)); + Ok(Self(Mutex::new(recv))) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + Err("using binds as wires is not (yet) supported for tcp".to_owned()) + } +} +impl crate::Wire for Wire { + type R = Rx3; + type T = Tx3; + async fn parse(arg: &str) -> Result { + Ok(Self(arg.to_owned())) + } + async fn open(&self) -> Result<(Self::R, Self::T), String> { + let arg = self.0.as_str(); + let pre = if arg.starts_with("ws://") || arg.starts_with("wss://") { + "" + } else { + "ws://" + }; + let (ws, _) = tokio_tungstenite::connect_async(format!("{pre}{arg}")) + .await + .map_err(|e| format!("[ws] connecting to `{pre}{arg}` failed: {e}"))?; + let (tx, rx) = ws.split(); + Ok((Rx3(rx), Tx3(tx))) + } +} + +impl crate::Rx for Rx1 { + type T<'a> = Vec; + + async fn rx<'a>(&mut self, _buf: &'a mut [u8]) -> Result, String> { + loop { + break match self.0.next().await { + None => Ok(Vec::new()), + Some(Ok(message)) => { + let bytes = message.into_bytes().to_vec(); + if bytes.is_empty() { + continue; + } + Ok(bytes) + } + Some(Err(e)) => Err(format!("[ws] receiving message failed: {e}")), + }; + } + } +} +impl crate::Tx for Tx1 { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + self.0 + .send(WsMessage::binary(buf.to_vec())) + .await + .map_err(|e| format!("[ws] writing bytes failed: {e}")) + } +} +impl crate::Rx for Rx3 { + type T<'a> = Vec; + + async fn rx<'a>(&mut self, _buf: &'a mut [u8]) -> Result, String> { + loop { + break match self.0.next().await { + None => Ok(Vec::new()), + Some(Ok(message)) => { + let bytes = message.into_data().to_vec(); + if bytes.is_empty() { + continue; + } + Ok(bytes) + } + Some(Err(e)) => Err(format!("[ws] receiving message failed: {e}")), + }; + } + } +} +impl crate::Tx for Tx3 { + async fn tx(&mut self, buf: &[u8]) -> Result<(), String> { + self.0 + .send(Message::binary(buf.to_vec())) + .await + .map_err(|e| format!("[ws] writing bytes failed: {e}")) + } +}