Merge pull request #1 from selimaj-dev/fully-functional

Fully functional
This commit is contained in:
2026-02-18 20:21:48 -08:00
committed by GitHub
11 changed files with 1456 additions and 0 deletions
+5
View File
@@ -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
Generated
+648
View File
@@ -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"
+12
View File
@@ -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"] }
+38
View File
@@ -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::<Data>("Hello from client".to_string())
.await?
);
println!(
"Invalid data response: {:?}",
session
.request::<Data>("invalid_data".to_string())
.await?
);
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
session.close().await?;
Ok(())
}
+48
View File
@@ -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::<Data, _>(async |_, req| {
println!("Msg from client: {req}");
if req == "invalid_data" {
return Err("Invalid data".to_string());
}
Ok("Hello from server".to_string())
})
.await;
});
}
}
+59
View File
@@ -0,0 +1,59 @@
use std::pin::Pin;
use serde::{Deserialize, Serialize};
pub mod server;
pub mod session;
pub mod ws;
pub type Result<T> = std::result::Result<T, Error>;
pub type BoxFuture<'a> = Pin<Box<dyn Future<Output = Option<(bool, serde_json::Value)>> + Send + 'a>>;
pub type MethodHandler = Box<dyn Fn(u32, serde_json::Value) -> 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<ws::Error> for Error {
fn from(value: ws::Error) -> Self {
Self::WebSocket(value)
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
}
}
impl From<tokio::sync::broadcast::error::RecvError> for Error {
fn from(value: tokio::sync::broadcast::error::RecvError) -> Self {
Self::RecvError(value)
}
}
+1
View File
@@ -0,0 +1 @@
pub struct SessionServer();
+198
View File
@@ -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<M: Method> {
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<Mutex<u32>>,
methods: Arc<Mutex<HashMap<String, MethodHandler>>>,
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<Self> {
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::<Message<GenericMethod>>(&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<Output = Result<M::Response, M::Error>> + 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<M: Method>(&self, data: &Message<M>) -> 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<M: Method>(
&self,
req: M::Request,
) -> crate::Result<std::result::Result<M::Response, M::Error>> {
let id = self.use_id().await;
self.send::<M>(&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::<GenericMethod>(&Message::Response {
id: to,
result: val,
})
.await
}
pub async fn respond_error(&self, to: u32, val: serde_json::Value) -> crate::Result<()> {
self.send::<GenericMethod>(&Message::ErrorResponse { id: to, error: val })
.await
}
pub async fn notify<M: Method>(&self, data: M::Request) -> crate::Result<()> {
self.send::<M>(&Message::Notification {
method: M::NAME.to_string(),
data,
})
.await
}
pub async fn close(&self) -> crate::Result<()> {
Ok(self.ws.close().await?)
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::string::FromUtf8Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
InvalidFrame(String),
HandshakeFailed(String),
Utf8(FromUtf8Error),
ConnectionClosed,
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<FromUtf8Error> for Error {
fn from(value: FromUtf8Error) -> Self {
Self::Utf8(value)
}
}
+172
View File
@@ -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<Self> {
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<Self> {
// 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,
})
}
}
+251
View File
@@ -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<u8>),
Ping,
Pong,
Close,
}
pub struct WebSocket {
pub(crate) reader: Arc<Mutex<tokio::net::tcp::OwnedReadHalf>>,
pub(crate) writer: Arc<Mutex<tokio::net::tcp::OwnedWriteHalf>>,
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<H: Hasher>(&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<u8>)> {
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<Frame> {
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}")))
}
}
}
}