diff --git a/.gitignore b/.gitignore index ad67955..0728338 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..743c953 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,648 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "session-rs" +version = "0.1.0" +dependencies = [ + "base64", + "rand", + "serde", + "serde_json", + "sha1", + "tokio", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[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.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[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 new file mode 100644 index 0000000..70031df --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "session-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +base64 = "0.22.1" +rand = "0.10.0" +serde = { version = "1.0.228", features = ["serde_derive"] } +serde_json = "1.0.149" +sha1 = "0.10.6" +tokio = { version = "1.49.0", features = ["io-util", "macros", "net", "rt", "sync", "time"] } diff --git a/examples/client.rs b/examples/client.rs new file mode 100644 index 0000000..14a1484 --- /dev/null +++ b/examples/client.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use session_rs::{Method, session::Session}; + +#[derive(Debug, Serialize, Deserialize)] +struct Data; + +impl Method for Data { + const NAME: &'static str = "data"; + type Request = String; + type Response = String; + type Error = String; +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> session_rs::Result<()> { + let session = Session::connect("127.0.0.1:8080", "/").await?; + + session.start_receiver(); + + println!( + "Hi: {:?}", + session + .request::("Hello from client".to_string()) + .await? + ); + + println!( + "Invalid data response: {:?}", + session + .request::("invalid_data".to_string()) + .await? + ); + + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + session.close().await?; + Ok(()) +} diff --git a/examples/server.rs b/examples/server.rs new file mode 100644 index 0000000..0d6fb8e --- /dev/null +++ b/examples/server.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; +use tokio::net::TcpListener; + +use session_rs::{Method, session::Session, ws::WebSocket}; + +#[derive(Debug, Serialize, Deserialize)] +struct Data; + +impl Method for Data { + const NAME: &'static str = "data"; + type Request = String; + type Response = String; + type Error = String; +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> session_rs::Result<()> { + let listener = TcpListener::bind("127.0.0.1:8080").await?; + println!("Server listening on ws://127.0.0.1:8080"); + + loop { + let (stream, addr) = listener.accept().await?; + println!("New connection: {}", addr); + + tokio::spawn(async move { + // Wrap session in Arc so tasks can share it + let session = Session::from_ws( + WebSocket::handshake(stream) + .await + .expect("Failed to initialize websocket"), + ); + + session.start_receiver(); + + session + .on::(async |_, req| { + println!("Msg from client: {req}"); + + if req == "invalid_data" { + return Err("Invalid data".to_string()); + } + + Ok("Hello from server".to_string()) + }) + .await; + }); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..310676f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,59 @@ +use std::pin::Pin; + +use serde::{Deserialize, Serialize}; + +pub mod server; +pub mod session; +pub mod ws; + +pub type Result = std::result::Result; +pub type BoxFuture<'a> = Pin> + Send + 'a>>; +pub type MethodHandler = Box BoxFuture<'static> + Send + Sync>; + +pub trait Method { + const NAME: &'static str; + type Request: Serialize + for<'de> Deserialize<'de> + Send + Sync; + type Response: Serialize + for<'de> Deserialize<'de>; + type Error: Serialize + for<'de> Deserialize<'de>; +} + +pub struct GenericMethod; + +impl Method for GenericMethod { + const NAME: &'static str = "generic_do_not_use"; + type Request = serde_json::Value; + type Response = serde_json::Value; + type Error = serde_json::Value; +} + +#[derive(Debug)] +pub enum Error { + WebSocket(ws::Error), + Json(serde_json::Error), + Io(std::io::Error), + RecvError(tokio::sync::broadcast::error::RecvError), +} + +impl From for Error { + fn from(value: ws::Error) -> Self { + Self::WebSocket(value) + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Json(value) + } +} + +impl From for Error { + fn from(value: tokio::sync::broadcast::error::RecvError) -> Self { + Self::RecvError(value) + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7754b94 --- /dev/null +++ b/src/server.rs @@ -0,0 +1 @@ +pub struct SessionServer(); diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..d20c7ca --- /dev/null +++ b/src/session.rs @@ -0,0 +1,198 @@ +use std::{collections::HashMap, sync::Arc}; + +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use tokio::sync::broadcast; + +use crate::{GenericMethod, Method, MethodHandler, ws::WebSocket}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase", tag = "type")] +pub enum Message { + Request { + id: u32, + method: String, + data: M::Request, + }, + Response { + id: u32, + result: M::Response, + }, + ErrorResponse { + id: u32, + error: M::Error, + }, + Notification { + method: String, + data: M::Request, + }, +} + +pub struct Session { + pub ws: WebSocket, + id: Arc>, + methods: Arc>>, + tx: broadcast::Sender<(u32, bool, serde_json::Value)>, +} + +impl Session { + pub fn clone(&self) -> Self { + Self { + ws: self.ws.clone(), + id: self.id.clone(), + methods: self.methods.clone(), + tx: self.tx.clone(), + } + } +} + +impl Session { + pub fn from_ws(ws: WebSocket) -> Self { + Self { + ws, + id: Arc::new(Mutex::new(0)), + methods: Arc::new(Mutex::new(HashMap::new())), + tx: broadcast::channel(8192).0, + } + } + + pub async fn connect(addr: &str, path: &str) -> crate::Result { + Ok(Self::from_ws(WebSocket::connect(addr, path).await?)) + } +} + +impl Session { + pub fn start_receiver(&self) { + let s = self.clone(); + tokio::spawn(async move { + loop { + match s.ws.read().await { + Ok(crate::ws::Frame::Text(text)) => { + let Ok(msg) = serde_json::from_str::>(&text) else { + continue; + }; + + match msg { + Message::Request { id, method, data } => { + if let Some(m) = s.methods.lock().await.get(&method) { + if let Some((err, res)) = (m)(id, data).await { + if err { + s.respond_error(id, res) + .await + .expect("Failed to respond"); + } else { + s.respond(id, res).await.expect("Failed to respond"); + } + } + } + } + Message::Response { id, result } => { + s.tx.send((id, false, result)).unwrap(); + } + Message::ErrorResponse { id, error } => { + s.tx.send((id, true, error)).unwrap(); + } + _ => {} + } + } + Ok(_) => {} + Err(_) => break, + } + } + }); + } + + pub async fn on< + M: Method, + Fut: Future> + Send + 'static, + >( + &self, + handler: impl Fn(u32, M::Request) -> Fut + Send + Sync + 'static, + ) { + let handler = Arc::new(handler); + + self.methods.lock().await.insert( + M::NAME.to_string(), + Box::new(move |id, value| { + let handler = Arc::clone(&handler); + + Box::pin(async move { + Some( + match handler(id, serde_json::from_value(value).ok()?).await { + Ok(v) => (false, serde_json::to_value(v).ok()?), + Err(v) => (true, serde_json::to_value(v).ok()?), + }, + ) + }) + }), + ); + } +} + +impl Session { + pub async fn send(&self, data: &Message) -> crate::Result<()> { + self.ws + .send_text_payload(&serde_json::to_vec(&data)?) + .await?; + Ok(()) + } + + pub async fn use_id(&self) -> u32 { + let mut id = self.id.lock().await; + *id += 1; + *id + } + + pub async fn request( + &self, + req: M::Request, + ) -> crate::Result> { + let id = self.use_id().await; + + self.send::(&Message::Request { + id, + method: M::NAME.to_string(), + data: req, + }) + .await?; + + let mut rx = self.tx.subscribe(); + + loop { + let r = rx.recv().await?; + + if r.0 == id { + break Ok(if r.1 { + Err(serde_json::from_value(r.2)?) + } else { + Ok(serde_json::from_value(r.2)?) + }); + } + } + } + + pub async fn respond(&self, to: u32, val: serde_json::Value) -> crate::Result<()> { + self.send::(&Message::Response { + id: to, + result: val, + }) + .await + } + + pub async fn respond_error(&self, to: u32, val: serde_json::Value) -> crate::Result<()> { + self.send::(&Message::ErrorResponse { id: to, error: val }) + .await + } + + pub async fn notify(&self, data: M::Request) -> crate::Result<()> { + self.send::(&Message::Notification { + method: M::NAME.to_string(), + data, + }) + .await + } + + pub async fn close(&self) -> crate::Result<()> { + Ok(self.ws.close().await?) + } +} diff --git a/src/ws/error.rs b/src/ws/error.rs new file mode 100644 index 0000000..9aa2299 --- /dev/null +++ b/src/ws/error.rs @@ -0,0 +1,24 @@ +use std::string::FromUtf8Error; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + InvalidFrame(String), + HandshakeFailed(String), + Utf8(FromUtf8Error), + ConnectionClosed, +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: FromUtf8Error) -> Self { + Self::Utf8(value) + } +} diff --git a/src/ws/handshake.rs b/src/ws/handshake.rs new file mode 100644 index 0000000..6b347fd --- /dev/null +++ b/src/ws/handshake.rs @@ -0,0 +1,172 @@ +use base64::Engine; +use sha1::{Digest, Sha1}; +use std::sync::Arc; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::TcpStream, + sync::Mutex, +}; + +use super::WebSocket; + +pub async fn handle_websocket_handshake(stream: &mut TcpStream) -> std::io::Result<()> { + let (read_half, mut write_half) = stream.split(); + let mut reader = BufReader::new(read_half); + + let mut request_line = String::new(); + reader.read_line(&mut request_line).await?; + let request_line = request_line.trim_end(); + + if request_line.starts_with("HEAD") { + write_half + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await?; + return Ok(()); + } + + if !request_line.starts_with("GET") { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid HTTP method", + )); + } + + use std::collections::HashMap; + let mut headers = HashMap::new(); + let mut line = String::new(); + + loop { + line.clear(); + reader.read_line(&mut line).await?; + if line == "\r\n" { + break; + } + if let Some((k, v)) = line.split_once(':') { + headers.insert(k.trim().to_lowercase(), v.trim().to_string()); + } + } + + if headers + .get("upgrade") + .map(|v| !v.eq_ignore_ascii_case("websocket")) + .unwrap_or(true) + { + write_half + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK") + .await?; + return Ok(()); + } + + let key = headers + .get("sec-websocket-key") + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing key"))?; + + use base64::Engine; + use base64::engine::general_purpose::STANDARD as Base64; + use sha1::{Digest, Sha1}; + + let mut hasher = Sha1::new(); + hasher.update(key.as_bytes()); + hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + let accept = Base64.encode(hasher.finalize()); + + let response = format!( + "HTTP/1.1 101 Switching Protocols\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-WebSocket-Accept: {}\r\n\r\n", + accept + ); + + write_half.write_all(response.as_bytes()).await?; + Ok(()) +} + +impl WebSocket { + pub async fn handshake(mut stream: TcpStream) -> super::Result { + handle_websocket_handshake(&mut stream).await?; + + let (read, write) = stream.into_split(); + + Ok(Self { + id: rand::random(), + reader: Arc::new(Mutex::new(read)), + writer: Arc::new(Mutex::new(write)), + is_server: false, + }) + } + + /// Connect to a WebSocket server and perform the handshake + pub async fn connect(addr: &str, path: &str) -> super::Result { + // 1. TCP connect + let mut stream = TcpStream::connect(addr).await?; + + // 2. Generate Sec-WebSocket-Key + let key_bytes: [u8; 16] = rand::random(); + let key = base64::prelude::BASE64_STANDARD.encode(&key_bytes); + + // 3. Send HTTP Upgrade request + let request = format!( + "GET {} HTTP/1.1\r\n\ + Host: {}\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-WebSocket-Key: {}\r\n\ + Sec-WebSocket-Version: 13\r\n\ + \r\n", + path, addr, key + ); + stream.write_all(request.as_bytes()).await?; + stream.flush().await?; + + // 4. Read HTTP response + let mut reader = BufReader::new(&mut stream); + let mut status_line = String::new(); + reader.read_line(&mut status_line).await?; + if !status_line.starts_with("HTTP/1.1 101") { + return Err(super::Error::HandshakeFailed(format!( + "Expected 101 Switching Protocols, got: {}", + status_line.trim_end() + ))); + } + + // Read headers + let mut sec_accept = None; + loop { + let mut line = String::new(); + reader.read_line(&mut line).await?; + let line = line.trim_end(); + if line.is_empty() { + break; // end of headers + } + if let Some((k, v)) = line.split_once(':') { + if k.eq_ignore_ascii_case("sec-websocket-accept") { + sec_accept = Some(v.trim().to_string()); + } + } + } + + // 5. Verify Sec-WebSocket-Accept + let expected = { + let mut sha1 = Sha1::new(); + sha1.update(key.as_bytes()); + sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + base64::prelude::BASE64_STANDARD.encode(sha1.finalize()) + }; + if sec_accept.as_deref() != Some(expected.as_str()) { + return Err(super::Error::HandshakeFailed( + "Sec-WebSocket-Accept mismatch".into(), + )); + } + + // 6. Upgrade succeeded, split stream + let (read, write) = stream.into_split(); + + Ok(Self { + id: rand::random(), + reader: Arc::new(Mutex::new(read)), + writer: Arc::new(Mutex::new(write)), + is_server: true, + }) + } +} diff --git a/src/ws/mod.rs b/src/ws/mod.rs new file mode 100644 index 0000000..6a6de66 --- /dev/null +++ b/src/ws/mod.rs @@ -0,0 +1,251 @@ +pub mod error; +pub mod handshake; +pub use error::{Error, Result}; + +use std::{ + hash::{Hash, Hasher}, + sync::Arc, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + sync::Mutex, +}; + +#[derive(Debug, Clone)] +pub enum Frame { + Text(String), + Binary(Vec), + Ping, + Pong, + Close, +} + +pub struct WebSocket { + pub(crate) reader: Arc>, + pub(crate) writer: Arc>, + pub(crate) id: u64, + pub(crate) is_server: bool, +} + +impl Clone for WebSocket { + fn clone(&self) -> Self { + WebSocket { + reader: self.reader.clone(), + writer: self.writer.clone(), + is_server: self.is_server.clone(), + id: self.id, + } + } +} + +impl PartialEq for WebSocket { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for WebSocket {} + +impl Hash for WebSocket { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl WebSocket { + async fn send_frame(&self, opcode: u8, payload: &[u8]) -> Result<()> { + let mut writer = self.writer.lock().await; + + let mut header = Vec::with_capacity(10); + let mask_bit = if self.is_server { 0x80 } else { 0x00 }; + header.push(0x80 | opcode); // FIN + opcode + + let len = payload.len(); + if len < 126 { + header.push((len as u8) | mask_bit); + } else if len <= 0xFFFF { + header.push(126 | mask_bit); + header.extend_from_slice(&(len as u16).to_be_bytes()); + } else { + header.push(127 | mask_bit); + header.extend_from_slice(&(len as u64).to_be_bytes()); + } + + if self.is_server { + // Generate 4-byte mask key + let mask_key: [u8; 4] = rand::random(); + header.extend_from_slice(&mask_key); + + // Mask the payload + let mut masked_payload = payload.to_vec(); + for i in 0..masked_payload.len() { + masked_payload[i] ^= mask_key[i % 4]; + } + + writer.write_all(&header).await?; + writer.write_all(&masked_payload).await?; + } else { + writer.write_all(&header).await?; + writer.write_all(payload).await?; + } + + writer.flush().await?; + Ok(()) + } +} + +impl WebSocket { + pub async fn send(&self, msg: &str) -> Result<()> { + self.send_frame(0x1, msg.as_bytes()).await + } + + pub async fn send_text_payload(&self, payload: &[u8]) -> Result<()> { + self.send_frame(0x1, payload).await + } + + pub async fn send_bin(&self, payload: &[u8]) -> Result<()> { + self.send_frame(0x2, payload).await + } + + pub async fn send_ping(&self) -> Result<()> { + self.send_frame(0x9, &[]).await + } + + pub async fn send_pong(&self) -> Result<()> { + self.send_frame(0xA, &[]).await + } + + pub async fn close(&self) -> Result<()> { + self.send_frame(0x8, &[]).await + } + + pub fn start_ping_loop(&self) { + let s = self.clone(); + tokio::task::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(15)); + loop { + interval.tick().await; + if s.send_ping().await.is_err() { + break; + } + } + }); + } +} + +impl WebSocket { + /// Read a full WebSocket frame (handling masking and control frames) + /// Returns (opcode, payload) + pub async fn read_frame(&self) -> Result<(bool, u8, Vec)> { + let mut reader = self.reader.lock().await; + + // --- 1. Read first 2-byte header --- + let mut header = [0u8; 2]; + reader.read_exact(&mut header).await?; + + let fin = header[0] & 0x80 != 0; + let opcode = header[0] & 0x0F; + let masked = header[1] & 0x80 != 0; + let mut payload_len = (header[1] & 0x7F) as u64; + + // --- 2. Read extended payload length if necessary --- + if payload_len == 126 { + let mut buf = [0u8; 2]; + reader.read_exact(&mut buf).await?; + payload_len = u16::from_be_bytes(buf) as u64; + } else if payload_len == 127 { + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf).await?; + payload_len = u64::from_be_bytes(buf); + } + + let payload = if masked { + // --- 3. Read mask key --- + let mut mask = [0u8; 4]; + reader.read_exact(&mut mask).await?; + let mut payload = vec![0u8; payload_len as usize]; + if payload_len > 0 { + reader.read_exact(&mut payload).await?; + for i in 0..payload.len() { + payload[i] ^= mask[i % 4]; + } + } + payload + } else { + // Per spec, client-to-server frames MUST be masked + if !self.is_server { + self.close().await.ok(); + return Err(Error::InvalidFrame( + "Received unmasked frame from client".into(), + )); + } + + let mut payload = vec![0u8; payload_len as usize]; + if payload_len > 0 { + reader.read_exact(&mut payload).await?; + } + payload + }; + + // --- 6. Return opcode + payload --- + Ok((fin, opcode, payload)) + } + + pub async fn read(&self) -> Result { + let (fin, opcode, mut payload) = self.read_frame().await?; + + if !fin { + // Continuation loop + while let (fin, o, mut p) = self.read_frame().await? + && !fin + { + match o { + // Continuation + 0x0 => payload.append(&mut p), + // Close + 0x8 => { + self.close().await.ok(); + } + // Ping + 0x9 => { + self.send_pong().await.ok(); + } + // Pong + 0xA => {} + _ => { + self.close().await.ok(); + return Err(Error::InvalidFrame(format!("Unknown opcode: {opcode}"))); + } + } + } + } + + match opcode { + // Close + 0x8 => { + self.close().await.ok(); + Ok(Frame::Close) + } + + // Ping + 0x9 => { + self.send_pong().await.ok(); + Ok(Frame::Ping) + } + + // Pong + 0xA => Ok(Frame::Pong), + + // Text + 0x1 => Ok(Frame::Text(String::from_utf8(payload)?)), + + // Binary + 0x2 => Ok(Frame::Binary(payload)), + + _ => { + self.close().await.ok(); + Err(Error::InvalidFrame(format!("Unknown opcode: {opcode}"))) + } + } + } +}