commit 379ae045d04f4610d9cb40cee60910ed2a4dc0de Author: core Date: Wed Aug 9 23:47:54 2023 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..717564f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,537 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[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 = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + +[[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 = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hornbeam" +version = "0.1.0" +dependencies = [ + "base64", + "log", + "radix64", + "rand", + "sha1", + "simple_logger", + "url", +] + +[[package]] +name = "hornbeam-client" +version = "0.1.0" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "radix64" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999718fa65c3be3a74f3f6dae5a98526ff436ea58a82a574f0de89eecd342bee" +dependencies = [ + "arrayref", + "cfg-if 0.1.10", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustix" +version = "0.38.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "simple_logger" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.42.0", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0f3942c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "hornbeam", + "hornbeam-client" +] \ No newline at end of file diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..6fd6d79 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["WebSocket", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "DirectX", "ECMAScript", "GPLv2", "GPLv3", "GitHub", "GitLab", "IPv4", "IPv6", "ClojureScript", "CoffeeScript", "JavaScript", "PureScript", "TypeScript", "NaN", "NaNs", "OAuth", "GraphQL", "OCaml", "OpenGL", "OpenMP", "OpenSSH", "OpenSSL", "OpenStreetMap", "OpenDNS", "WebGL", "TensorFlow", "TrueType", "iOS", "macOS", "FreeBSD", "TeX", "LaTeX", "BibTeX", "BibLaTeX", "MinGW", "CamelCase"] \ No newline at end of file diff --git a/hornbeam-client/Cargo.toml b/hornbeam-client/Cargo.toml new file mode 100644 index 0000000..e24ac99 --- /dev/null +++ b/hornbeam-client/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hornbeam-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hornbeam-client/src/lib.rs b/hornbeam-client/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/hornbeam-client/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/hornbeam/.gitignore b/hornbeam/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/hornbeam/.gitignore @@ -0,0 +1 @@ +/target diff --git a/hornbeam/Cargo.toml b/hornbeam/Cargo.toml new file mode 100644 index 0000000..7e02bd5 --- /dev/null +++ b/hornbeam/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "hornbeam" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +url = "2.3" +sha1 = "0.10" + +rand = { version = "0.8", optional = true } # feature: csprng-rand + +base64 = { version = "0.21", optional = true } # feature: base64-base64 +radix64 = { version = "0.6", optional = true } # feature: base64-radix64 + +log = { version = "0.4", optional = true } # feature: logging + +[dev-dependencies] +simple_logger = "4.1" + +[features] +default = ["csprng-rand", "base64-radix64", "logging"] + +csprng-rand = ["rand"] + +base64-base64 = ["base64"] +base64-radix64 = ["radix64"] + +logging = ["log"] \ No newline at end of file diff --git a/hornbeam/src/b64/b64_base64.rs b/hornbeam/src/b64/b64_base64.rs new file mode 100644 index 0000000..54bf3ad --- /dev/null +++ b/hornbeam/src/b64/b64_base64.rs @@ -0,0 +1,10 @@ +use base64::Engine; +use crate::b64::b64_common::DecodeError; + +pub(crate) fn base64_encode(b: &[u8]) -> String { + base64::engine::general_purpose::STANDARD.encode(b) +} + +pub(crate) fn base64_decode(b: &str) -> Result, DecodeError> { + Ok(base64::engine::general_purpose::STANDARD.decode(b)?) +} \ No newline at end of file diff --git a/hornbeam/src/b64/b64_common.rs b/hornbeam/src/b64/b64_common.rs new file mode 100644 index 0000000..16de4ca --- /dev/null +++ b/hornbeam/src/b64/b64_common.rs @@ -0,0 +1,54 @@ +use std::fmt::{Display, Formatter}; + +/// Errors that can occur during decoding. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DecodeError { + /// An invalid byte was found in the input. The offending byte is provided. + InvalidByte(u8), + /// The length of the input is invalid. + InvalidLength, + /// The last non-padding byte of input has discarded bits and those bits are + /// not zero. While this could be decoded it likely represents a corrupted or + /// invalid encoding. + InvalidTrailingBits, + /// invalid padding + InvalidPadding +} + +impl Display for DecodeError { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + DecodeError::InvalidByte(byte) => write!(f, "invalid byte {}", byte), + DecodeError::InvalidLength => write!(f, "encoded text cannot have a 6-bit remainder"), + DecodeError::InvalidTrailingBits => { + write!(f, "last byte has unnecessary trailing bits") + }, + DecodeError::InvalidPadding => { + write!(f, "invalid padding") + } + } + } +} + +#[cfg(feature = "base64-radix64")] +impl From for DecodeError { + fn from(value: radix64::DecodeError) -> Self { + match value { + radix64::DecodeError::InvalidByte(b) => Self::InvalidByte(b), + radix64::DecodeError::InvalidLength => Self::InvalidLength, + radix64::DecodeError::InvalidTrailingBits => Self::InvalidTrailingBits + } + } +} + +#[cfg(feature = "base64-base64")] +impl From for DecodeError { + fn from(value: base64::DecodeError) -> Self { + match value { + base64::DecodeError::InvalidByte(_, b) => Self::InvalidByte(b), + base64::DecodeError::InvalidLength => Self::InvalidLength, + base64::DecodeError::InvalidLastSymbol(_, _) => Self::InvalidTrailingBits, + base64::DecodeError::InvalidPadding => Self::InvalidPadding + } + } +} \ No newline at end of file diff --git a/hornbeam/src/b64/b64_radix64.rs b/hornbeam/src/b64/b64_radix64.rs new file mode 100644 index 0000000..c164ef2 --- /dev/null +++ b/hornbeam/src/b64/b64_radix64.rs @@ -0,0 +1,9 @@ +use crate::b64::b64_common::DecodeError; + +pub(crate) fn base64_encode(b: &[u8]) -> String { + radix64::STD.encode(b) +} + +pub(crate) fn base64_decode(b: &str) -> Result, DecodeError> { + Ok(radix64::STD.decode(b)?) +} \ No newline at end of file diff --git a/hornbeam/src/b64/mod.rs b/hornbeam/src/b64/mod.rs new file mode 100644 index 0000000..1845f27 --- /dev/null +++ b/hornbeam/src/b64/mod.rs @@ -0,0 +1,15 @@ +#[cfg(not(any(feature = "base64-base64", feature = "base64-radix64")))] +compile_error!("You need to select one Base64 implementation"); + +#[cfg(all(feature = "base64-base64", feature = "base64-radix64"))] +compile_error!("You can only select one Base64 implementation"); + +#[cfg(feature = "base64-base64")] +#[path = "b64_base64.rs"] +pub(crate) mod impl_b64; + +#[cfg(feature = "base64-radix64")] +#[path = "b64_radix64.rs"] +pub(crate) mod impl_b64; + +pub(crate) mod b64_common; \ No newline at end of file diff --git a/hornbeam/src/handshake_client.rs b/hornbeam/src/handshake_client.rs new file mode 100644 index 0000000..3883950 --- /dev/null +++ b/hornbeam/src/handshake_client.rs @@ -0,0 +1,331 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::io; +use std::io::{Read, Write}; +use std::string::FromUtf8Error; +use url::Url; +use crate::handshake_common::{HeaderMap, WEBSOCKET_PROTOCOL_VERSION}; +use crate::random::websocket_client_key; + +/// Contains the information needed to perform the WebSocket client handshake. Create from a URL with `ClientConnectionInfo::from(url)`, +/// or build from it's components with `ClientConnectionInfo::build()`. +pub struct ClientConnectionInfo { + url: Url, + websocket_key: [u8; 16], + origin: Option, + extra_headers: HeaderMap +} + +#[derive(Debug)] +/// Errors that can happen while sending the client handshake +pub enum ClientHandshakeSendError { + /// The URL scheme on the URL provided was invalid (not ws or wss) + InvalidUrlScheme { + /// The scheme provided + got_scheme: String + }, + /// The URL provided does not have a host + UrlMissingHost, + /// There was an IO error while trying to write the handshake + IoError(io::Error) +} +impl Display for ClientHandshakeSendError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ClientHandshakeSendError::InvalidUrlScheme { got_scheme } => { + write!(f, "invalid url scheme: expected one of 'ws', 'wss', got '{}'", got_scheme) + }, + ClientHandshakeSendError::UrlMissingHost => { + write!(f, "invalid url: url missing host") + } + ClientHandshakeSendError::IoError(e) => { + write!(f, "io error: {}", e) + } + } + } +} +impl Error for ClientHandshakeSendError {} + +impl From for ClientHandshakeSendError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} + +#[derive(Debug)] +/// Errors that can happen while receiving the client handshake response +pub enum ClientHandshakeRecvError { + /// There was an IO error while trying to read the handshake response + IoError(io::Error), + /// Reached an EOF while trying to read the handshake + UnexpectedEOF, + /// Received invalid UTF-8 data while trying to parse the handshake + InvalidUTF8(FromUtf8Error), + /// Server did not respond with a HTTP/1.1 response + IncorrectHTTPVersion { + /// The version that the server actually sent + server_response: String + }, + /// WebSocket handshake responses must be 4 lines at minimum, but this response was too short + ResponseTooShort, + /// WebSocket handshake responses must be valid HTTP responses, but the header was improperly formatted + IncorrectHTTPHeader, + /// The HTTP response indicated failure + HttpErrorCode { + /// The error code returned by the server + code: String, + /// The error code description returned by the server + message: String + }, + /// The handshake response was missing the `Connection: Upgrade` header + MissingConnectionUpgrade, + /// The handshake response was missing the `Upgrade: websocket` header + MissingUpgradeWebsocket, + /// The handshake response was missing the `Sec-WebSocket-Accept` header + MissingSecWebsocketAccept, + /// The handshake response had the `Sec-WebSocket-Accept` header, but it's value was incorrect + IncorrectSecWebsocketAccept +} +impl Display for ClientHandshakeRecvError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => { + write!(f, "io error: {e}") + }, + Self::UnexpectedEOF => { + write!(f, "unexpected EOF") + }, + Self::InvalidUTF8(e) => { + write!(f, "invalid UTF-8: {e}") + }, + Self::IncorrectHTTPVersion { server_response } => { + write!(f, "incorrect HTTP version while reading response, expected HTTP/1.1, got '{server_response}'") + }, + Self::ResponseTooShort => { + write!(f, "websocket response invalid (too short)") + }, + Self::IncorrectHTTPHeader => { + write!(f, "websocket response invalid (improper http response)") + }, + Self::HttpErrorCode { code, message } => { + write!(f, "websocket error: HTTP {code} {message}") + }, + Self::MissingConnectionUpgrade => write!(f, "websocket response invalid (missing Connection: Upgrade)"), + Self::MissingUpgradeWebsocket => write!(f, "websocket response invalid (missing Upgrade: websocket)"), + Self::MissingSecWebsocketAccept => write!(f, "websocket response invalid (missing Sec-WebSocket-Accept)"), + Self::IncorrectSecWebsocketAccept => write!(f, "websocket response invalid (Sec-WebSocket-Accept incorrect)") + } + } +} +impl Error for ClientHandshakeRecvError {} + +impl From for ClientHandshakeRecvError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} +impl From for ClientHandshakeRecvError { + fn from(value: FromUtf8Error) -> Self { + Self::InvalidUTF8(value) + } +} + +impl ClientConnectionInfo { + /// Creates a new `ClientConnectionInfo` with the connection target URL, an optional Origin header value, and + /// any headers you wish to set in the initial request (cookies, authentication, etc) + pub fn build(url: Url, origin: Option, extra_headers: HeaderMap) -> Self { + Self { + url, + websocket_key: websocket_client_key(), + origin, + extra_headers + } + } + + /// Send the WebSocket client handshake over the given stream. + /// # Errors + /// This function will return an error if the handshake fails for any reason. + /// See `ClientHandshakeError` for more details. + pub fn send_handshake(&self, w: &mut W) -> Result<(), ClientHandshakeSendError> { + let resource_url = self.url.path().to_string() + self.url.query().unwrap_or(""); + + // send the handshake: + // 1. HTTP GET + write!(w, "GET {resource_url} HTTP/1.1\r\n")?; + write!(w, "Host: {}\r\n", self.url.host_str().ok_or(ClientHandshakeSendError::UrlMissingHost)?)?; + write!(w, "Upgrade: websocket\r\n")?; + write!(w, "Connection: upgrade\r\n")?; + write!(w, "Sec-WebSocket-Key: {}\r\n", crate::b64::impl_b64::base64_encode(&self.websocket_key))?; + + if let Some(origin) = &self.origin { + write!(w, "Origin: {origin}\r\n")?; + } + write!(w, "Sec-WebSocket-Version: {WEBSOCKET_PROTOCOL_VERSION}\r\n")?; + + // TODO: permessage-deflate (7692) + + for (key, val) in &self.extra_headers { + write!(w, "{key}: {val}\r\n")?; + } + + write!(w, "\r\n")?; + + Ok(()) + } + + /// Read the WebSocket handshake response from the given stream. + /// # Errors + /// This function will return an error in many circumstances. See `ClientHandshakeRecvError` for details. + pub fn read_handshake(&self, r: &mut R) -> Result<(), ClientHandshakeRecvError> { + // read until we see the double \r\n + let mut read_bytes = vec![]; + let mut read_buf = [0u8; 1024]; + loop { + let read = r.read(&mut read_buf)?; + + if read == 0 { + return Err(ClientHandshakeRecvError::UnexpectedEOF); + } + + read_bytes.extend_from_slice(&read_buf[0..read-1]); + + if read_bytes.ends_with(&[0x0d, 0x0a, 0x0d]) || read_bytes.ends_with(&[0x0d, 0x0a, 0x0d, 0x0a]) { + // we have hit the end of the server ws handshake + break; + } + } + + debug!("read {:?} bytes from server, parsing as handshake", read_bytes); + + let handshake_response = String::from_utf8(read_bytes)?; + + trace!("{}", handshake_response); + + let lines = handshake_response.lines().collect::>(); + + if lines.len() < 4 { + return Err(ClientHandshakeRecvError::ResponseTooShort); + } + + let http_header_parts = lines[0].split(' ').collect::>(); + + if http_header_parts.len() < 3 { + return Err(ClientHandshakeRecvError::IncorrectHTTPHeader); + } + + if http_header_parts[0] != "HTTP/1.1" { + return Err(ClientHandshakeRecvError::IncorrectHTTPVersion { + server_response: http_header_parts[0].to_string() + }); + } + + if http_header_parts[1] != "101" { + let error_code = http_header_parts[1].to_string(); + let error_response = http_header_parts[2..].join(" "); + return Err(ClientHandshakeRecvError::HttpErrorCode { + code: error_code, + message: error_response + }); + } + + let mut has_connection = false; + let mut has_upgrade = false; + let mut has_sec_accept = false; + + for line in &lines[1..] { + if line.to_lowercase() == "connection: upgrade" { + has_connection = true; + } else if line.to_lowercase() == "upgrade: websocket" { + has_upgrade = true; + } else if line.to_lowercase().starts_with("sec-websocket-accept: ") { + let accept_key = line.split(' ').nth(1).unwrap(); + + + } + } + + if !has_connection { + return Err(ClientHandshakeRecvError::MissingConnectionUpgrade); + } + if !has_upgrade { + return Err(ClientHandshakeRecvError::MissingUpgradeWebsocket); + } + if !has_sec_accept { + return Err(ClientHandshakeRecvError::MissingSecWebsocketAccept); + } + + Ok(()) + } +} + +impl From for ClientConnectionInfo { + fn from(value: Url) -> Self { + Self::build( + value, + None, + HeaderMap::new() + ) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::HashMap; + use std::io::{Cursor}; + use std::net::TcpStream; + use std::str::FromStr; + use std::sync::Once; + use url::Url; + use crate::handshake_client::ClientConnectionInfo; + + static LOG: Once = Once::new(); + fn setup_logger() { + LOG.call_once(|| simple_logger::init().unwrap()); + } + + #[test] + fn client_handshake_test() { + setup_logger(); + + let mut client = ClientConnectionInfo::build( + Url::from_str("ws://somesite.com").unwrap(), + Some("whatever".to_string()), + HashMap::from([ + ("Authorization".to_string(), "Bearer 12345".to_string()) + ]) + ); + client.websocket_key = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10]; + + let mut fakesock = Cursor::new(Vec::new()); + + client.send_handshake(&mut fakesock).unwrap(); + + assert_eq!( + String::from_utf8(fakesock.into_inner()).unwrap(), + + "GET / HTTP/1.1\r\n\ + Host: somesite.com\r\n\ + Upgrade: websocket\r\n\ + Connection: upgrade\r\n\ + Sec-WebSocket-Key: AQIDBAUGBwgJCgsMDQ4PEA==\r\n\ + Origin: whatever\r\n\ + Sec-WebSocket-Version: 13\r\n\ + Authorization: Bearer 12345\r\n\r\n" + ); + } + + #[test] + fn live_client_test() { + setup_logger(); + + let client = ClientConnectionInfo::from(Url::from_str("ws://10.16.1.1:3204/ws").unwrap()); + + let mut tcpstream = TcpStream::connect("10.16.1.1:3204").unwrap(); + + client.send_handshake(&mut tcpstream).unwrap(); + client.read_handshake(&mut tcpstream).unwrap(); + + panic!(); + } +} \ No newline at end of file diff --git a/hornbeam/src/handshake_common.rs b/hornbeam/src/handshake_common.rs new file mode 100644 index 0000000..2bb8e04 --- /dev/null +++ b/hornbeam/src/handshake_common.rs @@ -0,0 +1,15 @@ +use std::collections::HashMap; +use sha1::{Sha1, Digest}; + +/// The WebSocket protocol version, as defined by RFC 6455 to be 13. +pub const WEBSOCKET_PROTOCOL_VERSION: i32 = 13; + +/// A type alias for a key-value header map +pub type HeaderMap = HashMap; + +pub(crate) fn derive_handshake_response(input: [u8; 16]) -> String { + let mut hasher = Sha1::new(); + hasher.update(input); + hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + crate::b64::impl_b64::base64_encode(&hasher.finalize()) +} \ No newline at end of file diff --git a/hornbeam/src/handshake_server.rs b/hornbeam/src/handshake_server.rs new file mode 100644 index 0000000..e69de29 diff --git a/hornbeam/src/lib.rs b/hornbeam/src/lib.rs new file mode 100644 index 0000000..8972bf4 --- /dev/null +++ b/hornbeam/src/lib.rs @@ -0,0 +1,84 @@ +//! # Hornbeam +//! Hornbeam is a simple WebSocket protocol library. For the client or server crates, check out +//! `hornbeam-client` or `hornbeam-server`. + +#![deny(clippy::correctness)] +#![warn(clippy::suspicious)] +#![warn(clippy::style)] +#![warn(clippy::complexity)] +#![deny(clippy::perf)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +//#![deny(clippy::unwrap_used)] +#![warn(clippy::expect_used)] +#![deny(missing_docs)] +#![allow(clippy::must_use_candidate)] // This gets annoying + +#[allow(unused)] +#[macro_use] +pub(crate) mod logging { + macro_rules! trace { + ($($x:tt)*) => ( + #[cfg(feature = "logging")] { + log::trace!($($x)*) + } + ) + } + + macro_rules! debug { + ($($x:tt)*) => ( + #[cfg(feature = "logging")] { + log::debug!($($x)*) + } + ) + } + + macro_rules! info { + ($($x:tt)*) => ( + #[cfg(feature = "logging")] { + log::info!($($x)*) + } + ) + } + + macro_rules! warn { + ($($x:tt)*) => ( + #[cfg(feature = "logging")] { + log::warn!($($x)*) + } + ) + } + + macro_rules! error { + ($($x:tt)*) => ( + #[cfg(feature = "logging")] { + log::error!($($x)*) + } + ) + } +} + +/// Contains the state definitions of the WebSocket state machine +pub mod state; + +/// Contains the common code for the client and server handshakes +pub mod handshake_common; + +/// Contains the code for the client handshake +pub mod handshake_client; + +/// Contains the code for the server handshake +pub mod handshake_server; + +/// Contains the generic WebSocket stream implementation, that streams are upgraded into after completing the client or server handshake +pub mod stream; + +#[cfg(not(feature = "csprng-rand"))] +compile_error!("You need to select one CSPRNG implementation"); + +/// Pluggable CSPRNG implementation +#[cfg(feature = "csprng-rand")] +#[path = "random_rand.rs"] +pub mod random; + +pub(crate) mod b64; \ No newline at end of file diff --git a/hornbeam/src/random_rand.rs b/hornbeam/src/random_rand.rs new file mode 100644 index 0000000..35208db --- /dev/null +++ b/hornbeam/src/random_rand.rs @@ -0,0 +1,6 @@ +use rand::Rng; + +/// Generates a random WebSocket client key +pub fn websocket_client_key() -> [u8; 16] { + rand::thread_rng().gen() +} \ No newline at end of file diff --git a/hornbeam/src/state.rs b/hornbeam/src/state.rs new file mode 100644 index 0000000..0e85299 --- /dev/null +++ b/hornbeam/src/state.rs @@ -0,0 +1,11 @@ +/// The state that the websocket stream is currently in +pub enum StreamState { + /// The stream is connecting - the handshake has not finished + Connecting, + /// The stream is open - data can be sent + Open, + /// The stream is closing - no more data can be sent + Closing, + /// The stream is closed + Closed +} \ No newline at end of file diff --git a/hornbeam/src/stream.rs b/hornbeam/src/stream.rs new file mode 100644 index 0000000..0c723a8 --- /dev/null +++ b/hornbeam/src/stream.rs @@ -0,0 +1,4 @@ +/// An upgraded TCP stream, used to send WebSocket frames. +pub struct WebsocketStream { + enable_masking: bool +} \ No newline at end of file diff --git a/tarpaulin-report.html b/tarpaulin-report.html new file mode 100644 index 0000000..4c1f133 --- /dev/null +++ b/tarpaulin-report.html @@ -0,0 +1,660 @@ + + + + + + + +
+ + + + + + \ No newline at end of file