2023-12-01 15:36:14 +00:00
|
|
|
use std::{sync::Arc, io::Read, time::Duration};
|
2023-11-03 09:45:31 +00:00
|
|
|
|
2023-06-29 08:14:29 +00:00
|
|
|
use async_trait::async_trait;
|
|
|
|
|
2023-11-07 04:45:39 +00:00
|
|
|
use tokio::sync::Mutex;
|
|
|
|
|
|
|
|
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
2023-11-06 16:45:31 +00:00
|
|
|
use simple_request::{
|
2023-11-07 04:45:39 +00:00
|
|
|
hyper::{StatusCode, header::HeaderValue, Request},
|
|
|
|
Response, Client,
|
2023-11-03 09:45:31 +00:00
|
|
|
};
|
2023-06-29 08:14:29 +00:00
|
|
|
|
|
|
|
use crate::rpc::{RpcError, RpcConnection, Rpc};
|
|
|
|
|
2023-12-01 15:36:14 +00:00
|
|
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
|
2023-10-26 16:45:39 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
enum Authentication {
|
2023-11-07 04:45:39 +00:00
|
|
|
// If unauthenticated, use a single client
|
2023-11-06 16:45:31 +00:00
|
|
|
Unauthenticated(Client),
|
2023-11-07 04:45:39 +00:00
|
|
|
// If authenticated, use a single client which supports being locked and tracks its nonce
|
2023-10-26 16:45:39 +00:00
|
|
|
// This ensures that if a nonce is requested, another caller doesn't make a request invalidating
|
|
|
|
// it
|
2023-11-07 04:45:39 +00:00
|
|
|
Authenticated {
|
|
|
|
username: String,
|
|
|
|
password: String,
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
|
|
connection: Arc<Mutex<(Option<(WwwAuthenticateHeader, u64)>, Client)>>,
|
|
|
|
},
|
2023-10-26 16:45:39 +00:00
|
|
|
}
|
|
|
|
|
2023-10-27 20:37:58 +00:00
|
|
|
/// An HTTP(S) transport for the RPC.
|
|
|
|
///
|
|
|
|
/// Requires tokio.
|
2023-06-29 08:14:29 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct HttpRpc {
|
2023-10-26 16:45:39 +00:00
|
|
|
authentication: Authentication,
|
2023-06-29 08:14:29 +00:00
|
|
|
url: String,
|
2023-12-01 15:36:14 +00:00
|
|
|
request_timeout: Duration,
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl HttpRpc {
|
2023-11-07 04:45:39 +00:00
|
|
|
fn digest_auth_challenge(
|
|
|
|
response: &Response,
|
|
|
|
) -> Result<Option<(WwwAuthenticateHeader, u64)>, RpcError> {
|
|
|
|
Ok(if let Some(header) = response.headers().get("www-authenticate") {
|
|
|
|
Some((
|
2023-11-29 05:36:58 +00:00
|
|
|
digest_auth::parse(header.to_str().map_err(|_| {
|
|
|
|
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
|
|
})?)
|
|
|
|
.map_err(|_| RpcError::InvalidNode("invalid digest-auth response".to_string()))?,
|
2023-11-07 04:45:39 +00:00
|
|
|
0,
|
|
|
|
))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-29 08:14:29 +00:00
|
|
|
/// Create a new HTTP(S) RPC connection.
|
|
|
|
///
|
|
|
|
/// A daemon requiring authentication can be used via including the username and password in the
|
|
|
|
/// URL.
|
2023-12-01 15:36:14 +00:00
|
|
|
pub async fn new(url: String) -> Result<Rpc<HttpRpc>, RpcError> {
|
2023-12-02 05:46:34 +00:00
|
|
|
Self::with_custom_timeout(url, DEFAULT_TIMEOUT).await
|
2023-12-01 15:36:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new HTTP(S) RPC connection with a custom timeout.
|
|
|
|
///
|
|
|
|
/// A daemon requiring authentication can be used via including the username and password in the
|
|
|
|
/// URL.
|
2023-12-02 05:46:34 +00:00
|
|
|
pub async fn with_custom_timeout(
|
2023-12-01 15:36:14 +00:00
|
|
|
mut url: String,
|
|
|
|
request_timeout: Duration,
|
|
|
|
) -> Result<Rpc<HttpRpc>, RpcError> {
|
2023-10-26 16:45:39 +00:00
|
|
|
let authentication = if url.contains('@') {
|
|
|
|
// Parse out the username and password
|
2023-06-29 08:14:29 +00:00
|
|
|
let url_clone = url;
|
|
|
|
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
|
|
|
if split_url.len() != 2 {
|
2023-11-03 09:45:31 +00:00
|
|
|
Err(RpcError::ConnectionError("invalid amount of login specifications".to_string()))?;
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
|
|
|
let mut userpass = split_url[0];
|
|
|
|
url = split_url[1].to_string();
|
|
|
|
|
|
|
|
// If there was additionally a protocol string, restore that to the daemon URL
|
|
|
|
if userpass.contains("://") {
|
|
|
|
let split_userpass = userpass.split("://").collect::<Vec<_>>();
|
|
|
|
if split_userpass.len() != 2 {
|
2023-11-03 09:45:31 +00:00
|
|
|
Err(RpcError::ConnectionError("invalid amount of protocol specifications".to_string()))?;
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
|
|
|
url = split_userpass[0].to_string() + "://" + &url;
|
|
|
|
userpass = split_userpass[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
let split_userpass = userpass.split(':').collect::<Vec<_>>();
|
2023-11-03 09:45:31 +00:00
|
|
|
if split_userpass.len() > 2 {
|
|
|
|
Err(RpcError::ConnectionError("invalid amount of passwords".to_string()))?;
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
2023-11-07 04:45:39 +00:00
|
|
|
|
|
|
|
let client = Client::without_connection_pool(url.clone())
|
|
|
|
.map_err(|_| RpcError::ConnectionError("invalid URL".to_string()))?;
|
|
|
|
// Obtain the initial challenge, which also somewhat validates this connection
|
|
|
|
let challenge = Self::digest_auth_challenge(
|
|
|
|
&client
|
|
|
|
.request(
|
|
|
|
Request::post(url.clone())
|
|
|
|
.body(vec![].into())
|
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))?,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
|
|
)?;
|
2023-11-06 16:45:31 +00:00
|
|
|
Authentication::Authenticated {
|
|
|
|
username: split_userpass[0].to_string(),
|
2023-12-17 01:54:24 +00:00
|
|
|
password: (*split_userpass.get(1).unwrap_or(&"")).to_string(),
|
2023-11-07 04:45:39 +00:00
|
|
|
connection: Arc::new(Mutex::new((challenge, client))),
|
2023-11-06 16:45:31 +00:00
|
|
|
}
|
2023-06-29 08:14:29 +00:00
|
|
|
} else {
|
2023-11-06 16:45:31 +00:00
|
|
|
Authentication::Unauthenticated(Client::with_connection_pool())
|
2023-06-29 08:14:29 +00:00
|
|
|
};
|
|
|
|
|
2023-12-01 15:36:14 +00:00
|
|
|
Ok(Rpc(HttpRpc { authentication, url, request_timeout }))
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-27 20:37:58 +00:00
|
|
|
impl HttpRpc {
|
2023-11-08 04:05:09 +00:00
|
|
|
async fn inner_post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
2023-11-07 04:45:39 +00:00
|
|
|
let request_fn = |uri| {
|
|
|
|
Request::post(uri)
|
|
|
|
.body(body.clone().into())
|
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))
|
|
|
|
};
|
2023-06-29 08:14:29 +00:00
|
|
|
|
2023-11-29 06:16:18 +00:00
|
|
|
async fn body_from_response(response: Response<'_>) -> Result<Vec<u8>, RpcError> {
|
|
|
|
/*
|
|
|
|
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::<u32>()
|
|
|
|
.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());
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
for attempt in 0 .. 2 {
|
2023-11-29 06:16:18 +00:00
|
|
|
return Ok(match &self.authentication {
|
|
|
|
Authentication::Unauthenticated(client) => {
|
|
|
|
body_from_response(
|
|
|
|
client
|
|
|
|
.request(request_fn(self.url.clone() + "/" + route)?)
|
|
|
|
.await
|
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
|
|
)
|
|
|
|
.await?
|
|
|
|
}
|
2023-11-08 04:05:09 +00:00
|
|
|
Authentication::Authenticated { username, password, connection } => {
|
|
|
|
let mut connection_lock = connection.lock().await;
|
2023-11-07 04:45:39 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
let mut request = request_fn("/".to_string() + route)?;
|
2023-11-07 04:45:39 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
// 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)?;
|
|
|
|
}
|
2023-11-07 04:45:39 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
// 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;
|
2023-11-07 04:45:39 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
let mut context = AuthContext::new_post::<_, _, _, &[u8]>(
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
"/".to_string() + route,
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes()));
|
2023-10-27 20:37:58 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
request.headers_mut().insert(
|
|
|
|
"Authorization",
|
|
|
|
HeaderValue::from_str(
|
|
|
|
&challenge
|
|
|
|
.respond(&context)
|
2023-11-29 05:36:58 +00:00
|
|
|
.map_err(|_| {
|
|
|
|
RpcError::InvalidNode("couldn't respond to digest-auth challenge".to_string())
|
|
|
|
})?
|
2023-11-08 04:05:09 +00:00
|
|
|
.to_header_string(),
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
);
|
|
|
|
}
|
2023-10-27 20:37:58 +00:00
|
|
|
|
2023-11-29 06:16:18 +00:00
|
|
|
let response = connection_lock
|
2023-11-08 04:05:09 +00:00
|
|
|
.1
|
|
|
|
.request(request)
|
|
|
|
.await
|
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")));
|
2023-11-07 04:45:39 +00:00
|
|
|
|
2023-11-29 06:16:18 +00:00
|
|
|
let (error, is_stale) = match &response {
|
|
|
|
Err(e) => (Some(e.clone()), false),
|
|
|
|
Ok(response) => (
|
|
|
|
None,
|
2023-11-08 04:05:09 +00:00
|
|
|
if response.status() == StatusCode::UNAUTHORIZED {
|
|
|
|
if let Some(header) = response.headers().get("www-authenticate") {
|
|
|
|
header
|
|
|
|
.to_str()
|
2023-11-29 05:36:58 +00:00
|
|
|
.map_err(|_| {
|
|
|
|
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
|
|
})?
|
2023-11-08 04:05:09 +00:00
|
|
|
.contains("stale")
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
2023-11-08 03:51:02 +00:00
|
|
|
} else {
|
|
|
|
false
|
2023-11-29 06:16:18 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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 error.is_some() || is_stale {
|
2023-11-08 04:05:09 +00:00
|
|
|
connection_lock.0 = None;
|
2023-11-29 06:16:18 +00:00
|
|
|
// If we're not already on our second attempt, move to the next loop iteration
|
|
|
|
// (retrying all of this once)
|
|
|
|
if attempt == 0 {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if let Some(e) = error {
|
|
|
|
Err(e)?
|
|
|
|
} else {
|
|
|
|
debug_assert!(is_stale);
|
|
|
|
Err(RpcError::InvalidNode(
|
|
|
|
"node claimed fresh connection had stale authentication".to_string(),
|
|
|
|
))?
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
body_from_response(response.unwrap()).await?
|
2023-11-08 04:05:09 +00:00
|
|
|
}
|
2023-11-07 04:45:39 +00:00
|
|
|
}
|
2023-11-29 06:16:18 +00:00
|
|
|
});
|
2023-11-08 04:05:09 +00:00
|
|
|
}
|
2023-10-27 20:37:58 +00:00
|
|
|
|
2023-11-08 04:05:09 +00:00
|
|
|
unreachable!()
|
2023-10-27 20:37:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl RpcConnection for HttpRpc {
|
|
|
|
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
2023-12-01 15:36:14 +00:00
|
|
|
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
2023-10-27 20:37:58 +00:00
|
|
|
.await
|
2023-11-06 16:45:31 +00:00
|
|
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
2023-06-29 08:14:29 +00:00
|
|
|
}
|
|
|
|
}
|