diff --git a/Cargo.lock b/Cargo.lock index 95df1016c..1441e97fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" + [[package]] name = "anyhow" version = "1.0.75" @@ -470,6 +476,26 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.39", +] + [[package]] name = "bitcoin" version = "0.31.0" @@ -675,6 +701,31 @@ dependencies = [ "serde_with", ] +[[package]] +name = "boring" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a46ed7bcfc8c7243686b870fb0a500ddca45596806088920429dd50aa4089f" +dependencies = [ + "bitflags 2.4.1", + "boring-sys", + "foreign-types", + "libc", + "once_cell", +] + +[[package]] +name = "boring-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a25b0f5d733611b9cc685db5df9dabfc8cdf7e1475518f7f966ce5eb8a0365" +dependencies = [ + "bindgen 0.68.1", + "cmake", + "fs_extra", + "fslock", +] + [[package]] name = "borsh" version = "1.2.0" @@ -992,6 +1043,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -2313,6 +2373,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "fork-tree" version = "3.0.0" @@ -2548,6 +2635,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "funty" version = "2.0.0" @@ -3053,6 +3156,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-boring" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e7bdbf33719f0fc0ea8131870aeaf57e885e381058c01c5422e5b3c467c56f" +dependencies = [ + "antidote", + "boring", + "http", + "hyper", + "linked_hash_set", + "once_cell", + "tokio", + "tokio-boring", + "tower-layer", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -4060,7 +4180,7 @@ version = "0.11.0+8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" dependencies = [ - "bindgen", + "bindgen 0.65.1", "bzip2-sys", "cc", "glob", @@ -4879,7 +4999,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 2.0.39", @@ -8182,7 +8302,7 @@ version = "0.1.0" dependencies = [ "base64ct", "hyper", - "hyper-rustls", + "hyper-boring", "tokio", "zeroize", ] @@ -9448,6 +9568,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-boring" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23acf051bc43dd3862ef7b13a8e0f7f274316a17ca77a265dda0b2bea9d1f7" +dependencies = [ + "boring", + "boring-sys", + "once_cell", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.2.0" diff --git a/coins/bitcoin/src/rpc.rs b/coins/bitcoin/src/rpc.rs index 73a215e04..bde7252ff 100644 --- a/coins/bitcoin/src/rpc.rs +++ b/coins/bitcoin/src/rpc.rs @@ -62,7 +62,8 @@ impl Rpc { /// provided to this library, if the RPC has an incompatible argument layout. That is not checked /// at time of RPC creation. pub async fn new(url: String) -> Result { - let rpc = Rpc { client: Client::with_connection_pool(), url }; + let rpc = + Rpc { client: Client::with_connection_pool().map_err(|_| RpcError::ConnectionError)?, url }; // Make an RPC request to verify the node is reachable and sane let res: String = rpc.rpc_call("help", json!([])).await?; diff --git a/coins/monero/src/rpc/http.rs b/coins/monero/src/rpc/http.rs index dad2c107a..fab2a27eb 100644 --- a/coins/monero/src/rpc/http.rs +++ b/coins/monero/src/rpc/http.rs @@ -6,7 +6,7 @@ use tokio::sync::Mutex; use digest_auth::{WwwAuthenticateHeader, AuthContext}; use simple_request::{ - hyper::{StatusCode, header::HeaderValue, Request}, + hyper::{header::HeaderValue, Request}, Response, Client, }; @@ -104,7 +104,9 @@ impl HttpRpc { connection: Arc::new(Mutex::new((challenge, client))), } } else { - Authentication::Unauthenticated(Client::with_connection_pool()) + Authentication::Unauthenticated( + Client::with_connection_pool().map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?, + ) }; Ok(Rpc(HttpRpc { authentication, url })) @@ -119,132 +121,101 @@ impl HttpRpc { .map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}"))) }; - for attempt in 0 .. 2 { - let response = match &self.authentication { - Authentication::Unauthenticated(client) => client - .request(request_fn(self.url.clone() + "/" + route)?) - .await - .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?, - Authentication::Authenticated { username, password, connection } => { - let mut connection_lock = connection.lock().await; - - let mut request = request_fn("/".to_string() + route)?; - - // If we don't have an auth challenge, obtain one - if connection_lock.0.is_none() { - connection_lock.0 = Self::digest_auth_challenge( - &connection_lock - .1 - .request(request) - .await - .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?, - )?; - request = request_fn("/".to_string() + route)?; - } - - // Insert the challenge response, if we have a challenge - if let Some((challenge, cnonce)) = connection_lock.0.as_mut() { - // Update the cnonce - // Overflow isn't a concern as this is a u64 - *cnonce += 1; - - let mut context = AuthContext::new_post::<_, _, _, &[u8]>( - username, - password, - "/".to_string() + route, - None, - ); - context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes())); - - request.headers_mut().insert( - "Authorization", - HeaderValue::from_str( - &challenge - .respond(&context) - .map_err(|_| RpcError::InvalidNode("couldn't respond to digest-auth challenge"))? - .to_header_string(), - ) - .unwrap(), - ); - } - - let response_result = connection_lock - .1 - .request(request) - .await - .map_err(|e| RpcError::ConnectionError(format!("{e:?}"))); + let response = match &self.authentication { + Authentication::Unauthenticated(client) => client + .request(request_fn(self.url.clone() + "/" + route)?) + .await + .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?, + Authentication::Authenticated { username, password, connection } => { + let mut connection_lock = connection.lock().await; + + let mut request = request_fn("/".to_string() + route)?; + + // If we don't have an auth challenge, obtain one + if connection_lock.0.is_none() { + connection_lock.0 = Self::digest_auth_challenge( + &connection_lock + .1 + .request(request) + .await + .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?, + )?; + request = request_fn("/".to_string() + route)?; + } - // If the connection entered an error state, drop the cached challenge as challenges are - // per-connection - // We don't need to create a new connection as simple-request will for us - if response_result.is_err() { - connection_lock.0 = None; - } + // Insert the challenge response, if we have a challenge + if let Some((challenge, cnonce)) = connection_lock.0.as_mut() { + // Update the cnonce + // Overflow isn't a concern as this is a u64 + *cnonce += 1; + + let mut context = AuthContext::new_post::<_, _, _, &[u8]>( + username, + password, + "/".to_string() + route, + None, + ); + context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes())); + + request.headers_mut().insert( + "Authorization", + HeaderValue::from_str( + &challenge + .respond(&context) + .map_err(|_| RpcError::InvalidNode("couldn't respond to digest-auth challenge"))? + .to_header_string(), + ) + .unwrap(), + ); + } - // If we're not already on our second attempt and: - // A) We had a connection error - // B) We need to re-auth due to this token being stale - // Move to the next loop iteration (retrying all of this) - if (attempt == 0) && - (response_result.is_err() || { - let response = response_result.as_ref().unwrap(); - if response.status() == StatusCode::UNAUTHORIZED { - if let Some(header) = response.headers().get("www-authenticate") { - header - .to_str() - .map_err(|_| RpcError::InvalidNode("www-authenticate header wasn't a string"))? - .contains("stale") - } else { - false - } - } else { - false - } - }) - { - // Drop the cached authentication before we do - connection_lock.0 = None; - continue; - } + let response_result = connection_lock + .1 + .request(request) + .await + .map_err(|e| RpcError::ConnectionError(format!("{e:?}"))); - response_result? + // If the connection entered an error state, drop the cached challenge as challenges are + // per-connection + // We don't need to create a new connection as simple-request will for us + if response_result.is_err() { + connection_lock.0 = None; } - }; - /* - let length = usize::try_from( - response - .headers() - .get("content-length") - .ok_or(RpcError::InvalidNode("no content-length header"))? - .to_str() - .map_err(|_| RpcError::InvalidNode("non-ascii content-length value"))? - .parse::() - .map_err(|_| RpcError::InvalidNode("non-u32 content-length value"))?, - ) - .unwrap(); - // Only pre-allocate 1 MB so a malicious node which claims a content-length of 1 GB actually - // has to send 1 GB of data to cause a 1 GB allocation - let mut res = Vec::with_capacity(length.max(1024 * 1024)); - let mut body = response.into_body(); - while res.len() < length { - let Some(data) = body.data().await else { break }; - res.extend(data.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?.as_ref()); + response_result? } - */ + }; - let mut res = Vec::with_capacity(128); + /* + let length = usize::try_from( response - .body() - .await - .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))? - .read_to_end(&mut res) - .unwrap(); - - return Ok(res); + .headers() + .get("content-length") + .ok_or(RpcError::InvalidNode("no content-length header"))? + .to_str() + .map_err(|_| RpcError::InvalidNode("non-ascii content-length value"))? + .parse::() + .map_err(|_| RpcError::InvalidNode("non-u32 content-length value"))?, + ) + .unwrap(); + // Only pre-allocate 1 MB so a malicious node which claims a content-length of 1 GB actually + // has to send 1 GB of data to cause a 1 GB allocation + let mut res = Vec::with_capacity(length.max(1024 * 1024)); + let mut body = response.into_body(); + while res.len() < length { + let Some(data) = body.data().await else { break }; + res.extend(data.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?.as_ref()); } + */ - unreachable!() + let mut res = Vec::with_capacity(128); + response + .body() + .await + .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))? + .read_to_end(&mut res) + .unwrap(); + Ok(res) } } diff --git a/common/request/Cargo.toml b/common/request/Cargo.toml index 7fd694253..f4baab988 100644 --- a/common/request/Cargo.toml +++ b/common/request/Cargo.toml @@ -18,12 +18,12 @@ rustdoc-args = ["--cfg", "docsrs"] hyper = { version = "0.14", default-features = false, features = ["http1", "tcp", "client", "runtime", "backports", "deprecated"] } tokio = { version = "1", default-features = false } -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "native-tokio"], optional = true } +hyper-boring = { version = "4", default-features = false, features = ["runtime"], optional = true } zeroize = { version = "1", optional = true } base64ct = { version = "1", features = ["alloc"], optional = true } [features] -tls = ["hyper-rustls"] +tls = ["hyper-boring"] basic-auth = ["zeroize", "base64ct"] default = ["tls"] diff --git a/common/request/src/lib.rs b/common/request/src/lib.rs index 1764ece27..8b51941e2 100644 --- a/common/request/src/lib.rs +++ b/common/request/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use tokio::sync::Mutex; #[cfg(feature = "tls")] -use hyper_rustls::{HttpsConnectorBuilder, HttpsConnector}; +use hyper_boring::HttpsConnector; use hyper::{ Uri, header::HeaderValue, @@ -36,11 +36,16 @@ type Connector = HttpConnector; #[cfg(feature = "tls")] type Connector = HttpsConnector; -#[derive(Clone, Debug)] +#[derive(Clone)] enum Connection { ConnectionPool(hyper::Client), Connection { connector: Connector, host: Uri, connection: Arc>>> }, } +impl core::fmt::Debug for Connection { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt.debug_struct("Connection").finish_non_exhaustive() + } +} #[derive(Clone, Debug)] pub struct Client { @@ -48,25 +53,24 @@ pub struct Client { } impl Client { - fn connector() -> Connector { + fn connector() -> Result { #[cfg(feature = "tls")] - let res = - HttpsConnectorBuilder::new().with_native_roots().https_or_http().enable_http1().build(); + let res = HttpsConnector::new().map_err(|e| Error::ConnectionError(format!("{e:?}").into()))?; #[cfg(not(feature = "tls"))] let res = HttpConnector::new(); - res + Ok(res) } - pub fn with_connection_pool() -> Client { - Client { - connection: Connection::ConnectionPool(hyper::Client::builder().build(Self::connector())), - } + pub fn with_connection_pool() -> Result { + Ok(Client { + connection: Connection::ConnectionPool(hyper::Client::builder().build(Self::connector()?)), + }) } pub fn without_connection_pool(host: String) -> Result { Ok(Client { connection: Connection::Connection { - connector: Self::connector(), + connector: Self::connector()?, host: { let uri: Uri = host.parse().map_err(|_| Error::InvalidUri)?; if uri.host().is_none() { diff --git a/message-queue/src/client.rs b/message-queue/src/client.rs index ed1031022..8dd078365 100644 --- a/message-queue/src/client.rs +++ b/message-queue/src/client.rs @@ -45,7 +45,7 @@ impl MessageQueue { service, pub_key: Ristretto::generator() * priv_key.deref(), priv_key, - client: Client::with_connection_pool(), + client: Client::with_connection_pool().unwrap(), url, } }