diff --git a/Cargo.lock b/Cargo.lock index 3e8c51e..01e1e95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,8 +39,8 @@ dependencies = [ "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.25", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -75,7 +75,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" dependencies = [ "bytestring", - "http", + "http 0.2.12", "regex", "serde", "tracing", @@ -916,7 +916,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -952,6 +971,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -996,6 +1021,40 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1014,6 +1073,62 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.3", + "http 1.1.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1040,6 +1155,13 @@ dependencies = [ [[package]] name = "icrc-client" version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "thiserror", + "tokio", + "uuid", +] [[package]] name = "icrc-server" @@ -1059,6 +1181,7 @@ dependencies = [ "rand", "serde", "sqlx", + "strum", "thiserror", "tokio", "uuid", @@ -1084,6 +1207,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.12.1" @@ -1324,6 +1453,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1438,6 +1577,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1575,6 +1734,48 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +[[package]] +name = "reqwest" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.3", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1623,6 +1824,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.17" @@ -2028,6 +2244,28 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.57", +] + [[package]] name = "subtle" version = "2.5.0" @@ -2056,6 +2294,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2144,13 +2409,36 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" @@ -2176,6 +2464,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -2208,6 +2524,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -2291,6 +2613,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2328,6 +2659,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -2357,6 +2700,16 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.5.1" @@ -2508,6 +2861,16 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/client/Cargo.toml b/client/Cargo.toml index 2ca6feb..c2651b0 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +reqwest = { version = "0.12.2", features = ["json"] } +serde = { version = "1.0.197", features = ["derive"] } +thiserror = "1.0.58" +tokio = { version = "1.37.0", features = ["full"] } +uuid = "1.8.0" diff --git a/client/src/error.rs b/client/src/error.rs new file mode 100644 index 0000000..41a8688 --- /dev/null +++ b/client/src/error.rs @@ -0,0 +1,17 @@ +#[derive(Debug, thiserror::Error, Clone)] +pub enum Error { + #[error("Failed to handle a server response: {0}")] + BadResponse(String), + + #[error("Failed to communicate with the host")] + ConnectionError, + + #[error("Failed to authorize the session with the given token")] + InvalidToken, + + #[error("The token used to authenticate the session is expired")] + TokenExpired, + + #[error("{0}")] + Unknown(String), +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 7d12d9a..3711d10 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,14 +1,21 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +mod error; +mod session; + +pub use session::Session; #[cfg(test)] mod tests { use super::*; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + #[tokio::test] + async fn authorized_session() { + let host = std::env::var("ICRC_TEST_HOST") + .expect("The environment variable ICRC_TEST_HOST needs to be set!"); + let token = std::env::var("ICRC_TEST_AUTH") + .expect("The environment variable ICRC_TEST_AUTH needs to be set!"); + + let session = Session::new(&host, Some(token)).await.unwrap(); + + // assert_eq!(result, 4); } } diff --git a/client/src/session/data.rs b/client/src/session/data.rs new file mode 100644 index 0000000..5bb4ada --- /dev/null +++ b/client/src/session/data.rs @@ -0,0 +1,52 @@ +use serde::Deserialize; + +pub mod api { + use super::Deserialize; + + pub mod account { + use super::Deserialize; + + pub mod info { + use super::Deserialize; + + #[derive(Debug, Deserialize)] + pub struct Response { + pub userid: String, + // TODO: make permissions an enum + pub permissions: Vec, + } + } + } + + pub mod error { + use super::Deserialize; + + #[derive(Debug, thiserror::Error, Deserialize)] + pub enum Error { + #[error("Failed to authenticate with the given credentials")] + AuthenticationFailure, + + #[error("The given token is invalid")] + InvalidToken, + + #[error("Permission denied: {0}")] + PermissionDenied(String), + + #[error("The given relay id cannot be mapped to a relay")] + RelayNotFound, + + #[error("The given token is expired")] + TokenExpired, + + #[error("The given user cannot be found")] + UserNotFound, + } + + #[derive(Debug, Deserialize)] + pub struct Body { + pub error: Error, + #[serde(skip)] + description: String, + } + } +} diff --git a/client/src/session/mod.rs b/client/src/session/mod.rs new file mode 100644 index 0000000..ca4e73b --- /dev/null +++ b/client/src/session/mod.rs @@ -0,0 +1,90 @@ +pub(self) mod data; + +use crate::error::Error; +use std::str::FromStr; +use uuid::Uuid; + +struct AuthorizedSession { + token: String, + userid: Uuid, +} + +/// A Session is a connection to an ICRC Server. +/// It can be either authorized or unauthorized. +/// An unauthorized session is not linked to any account on the server, +/// but an authorized session uses Bearer Authentication to get access to resources, +/// which are not publicly accessible. +pub struct Session { + host: String, + client: reqwest::Client, + + auth: Option, +} + +impl Session { + /// Creates a new [Session] with `host` as the ICRC Server host. + /// Giving `auth` the value [None] sets the session in an unauthorized state, + /// whilst [Some]`(token)` calls [Session::auth_with_token]. + pub async fn new(mut host: &str, auth: Option) -> Result { + if let Some(stripped) = host.strip_suffix("/") { + host = stripped; + } + + let mut session = Self { + host: host.to_string(), + client: reqwest::Client::new(), + auth: None, + }; + if let Some(token) = auth { + session.auth_with_token(token).await?; + } + + Ok(session) + } + + /// Tries to authorize the session. If `token` is not accepted by the server, this leads to an + /// [crate::error::Error::InvalidToken] or [crate::error::Error::TokenExpired] error. + pub async fn auth_with_token(&mut self, token: String) -> Result<(), Error> { + let request = self + .client + .get(format!("{host}/account/info", host = self.host)) + .bearer_auth(&token); + + let response = match request.send().await { + Ok(resp) => { + if resp.status().is_success() { + resp.json::() + .await + .map_err(|e| Error::BadResponse(e.to_string()))? + } else { + return Err( + match resp + .json::() + .await + .map_err(|e| Error::BadResponse(e.to_string()))? + .error + { + data::api::error::Error::InvalidToken => Error::InvalidToken, + data::api::error::Error::TokenExpired => Error::TokenExpired, + e => Error::BadResponse(format!("Unknown Error/{e} in this context")), + }, + ); + } + } + Err(e) => { + if e.is_connect() || e.is_timeout() { + return Err(Error::ConnectionError); + } + return Err(Error::Unknown(e.to_string())); + } + }; + + self.auth = Some(AuthorizedSession { + token, + userid: Uuid::from_str(&response.userid) + .map_err(|_| Error::BadResponse("Failed to parse userid".to_string()))?, + }); + + Ok(()) + } +}