feat(server): improve client builder
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -224,6 +224,7 @@ dependencies = [
|
|||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
"rusty-s3",
|
"rusty-s3",
|
||||||
"safefetch",
|
"safefetch",
|
||||||
"schemars",
|
"schemars",
|
||||||
@@ -239,6 +240,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"v_htmlescape",
|
"v_htmlescape",
|
||||||
|
"webpki-roots 1.0.6",
|
||||||
"y-octo",
|
"y-octo",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ rand = { workspace = true }
|
|||||||
reqwest = { version = "0.13.4", default-features = false, features = [
|
reqwest = { version = "0.13.4", default-features = false, features = [
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"std",
|
||||||
|
"tls12",
|
||||||
|
] }
|
||||||
rusty-s3 = "0.10.0"
|
rusty-s3 = "0.10.0"
|
||||||
safefetch = { workspace = true }
|
safefetch = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
@@ -63,6 +68,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "sync"] }
|
|||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
v_htmlescape = { workspace = true }
|
v_htmlescape = { workspace = true }
|
||||||
|
webpki-roots = "1.0"
|
||||||
y-octo = { workspace = true, features = ["large_refs"] }
|
y-octo = { workspace = true, features = ["large_refs"] }
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
future::Future,
|
|
||||||
pin::Pin,
|
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,6 +8,7 @@ use reqwest::{
|
|||||||
Client as ReqwestClient, Method, StatusCode,
|
Client as ReqwestClient, Method, StatusCode,
|
||||||
header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, HeaderMap, HeaderName, HeaderValue, LAST_MODIFIED},
|
header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, HeaderMap, HeaderName, HeaderValue, LAST_MODIFIED},
|
||||||
};
|
};
|
||||||
|
use rustls::RootCertStore;
|
||||||
use rusty_s3::{
|
use rusty_s3::{
|
||||||
Bucket, Credentials,
|
Bucket, Credentials,
|
||||||
actions::{
|
actions::{
|
||||||
@@ -31,8 +30,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000;
|
|||||||
const MAX_MULTIPART_PART_NUMBER: i32 = 10_000;
|
const MAX_MULTIPART_PART_NUMBER: i32 = 10_000;
|
||||||
const MAX_RESPONSE_BODY_BYTES: usize = i32::MAX as usize;
|
const MAX_RESPONSE_BODY_BYTES: usize = i32::MAX as usize;
|
||||||
|
|
||||||
type StorageHttpFuture<'a> = Pin<Box<dyn Future<Output = ObjectStorageResult<StorageHttpResponse>> + Send + 'a>>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct StorageHttpRequest {
|
struct StorageHttpRequest {
|
||||||
method: Method,
|
method: Method,
|
||||||
@@ -48,10 +45,6 @@ struct StorageHttpResponse {
|
|||||||
body: Vec<u8>,
|
body: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait StorageHttpClient: Clone + Send + Sync + 'static {
|
|
||||||
fn execute(&self, request: StorageHttpRequest) -> StorageHttpFuture<'_>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ReqwestStorageHttpClient {
|
struct ReqwestStorageHttpClient {
|
||||||
client: ReqwestClient,
|
client: ReqwestClient,
|
||||||
@@ -59,50 +52,61 @@ struct ReqwestStorageHttpClient {
|
|||||||
|
|
||||||
impl ReqwestStorageHttpClient {
|
impl ReqwestStorageHttpClient {
|
||||||
fn new(request_timeout_ms: Option<u64>) -> ObjectStorageResult<Self> {
|
fn new(request_timeout_ms: Option<u64>) -> ObjectStorageResult<Self> {
|
||||||
let builder = ReqwestClient::builder().timeout(Duration::from_millis(
|
let builder = ReqwestClient::builder()
|
||||||
request_timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS),
|
.tls_backend_preconfigured(Self::webpki_tls_config()?)
|
||||||
));
|
.timeout(Duration::from_millis(
|
||||||
|
request_timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS),
|
||||||
|
));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client: builder.build().map_err(ObjectStorageError::HttpClientBuild)?,
|
client: builder.build().map_err(ObjectStorageError::HttpClientBuild)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl StorageHttpClient for ReqwestStorageHttpClient {
|
fn webpki_tls_config() -> ObjectStorageResult<rustls::ClientConfig> {
|
||||||
fn execute(&self, request: StorageHttpRequest) -> StorageHttpFuture<'_> {
|
let roots = RootCertStore {
|
||||||
Box::pin(async move {
|
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||||
let mut builder = self.client.request(request.method, request.url);
|
};
|
||||||
for (key, value) in request.headers {
|
Ok(
|
||||||
let name =
|
rustls::ClientConfig::builder_with_provider(rustls::crypto::aws_lc_rs::default_provider().into())
|
||||||
HeaderName::from_bytes(key.as_bytes()).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?;
|
.with_safe_default_protocol_versions()
|
||||||
let value = HeaderValue::from_str(&value).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?;
|
.map_err(|err| ObjectStorageError::Config(format!("ObjectStorage TLS config failed: {err}")))?
|
||||||
builder = builder.header(name, value);
|
.with_root_certificates(roots)
|
||||||
}
|
.with_no_client_auth(),
|
||||||
if let Some(body) = request.body {
|
)
|
||||||
builder = builder.body(body);
|
}
|
||||||
}
|
|
||||||
let mut response = builder.send().await.map_err(ObjectStorageError::HttpRequest)?;
|
async fn execute(&self, request: StorageHttpRequest) -> ObjectStorageResult<StorageHttpResponse> {
|
||||||
let status = response.status();
|
let mut builder = self.client.request(request.method, request.url);
|
||||||
let headers = response.headers().clone();
|
for (key, value) in request.headers {
|
||||||
if response
|
let name =
|
||||||
.content_length()
|
HeaderName::from_bytes(key.as_bytes()).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?;
|
||||||
.is_some_and(|length| length > request.max_response_body_bytes as u64)
|
let value = HeaderValue::from_str(&value).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?;
|
||||||
{
|
builder = builder.header(name, value);
|
||||||
|
}
|
||||||
|
if let Some(body) = request.body {
|
||||||
|
builder = builder.body(body);
|
||||||
|
}
|
||||||
|
let mut response = builder.send().await.map_err(ObjectStorageError::HttpRequest)?;
|
||||||
|
let status = response.status();
|
||||||
|
let headers = response.headers().clone();
|
||||||
|
if response
|
||||||
|
.content_length()
|
||||||
|
.is_some_and(|length| length > request.max_response_body_bytes as u64)
|
||||||
|
{
|
||||||
|
return Err(ObjectStorageError::BodyTooLarge {
|
||||||
|
limit: request.max_response_body_bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut body = Vec::new();
|
||||||
|
while let Some(chunk) = response.chunk().await.map_err(ObjectStorageError::HttpRequest)? {
|
||||||
|
if body.len() + chunk.len() > request.max_response_body_bytes {
|
||||||
return Err(ObjectStorageError::BodyTooLarge {
|
return Err(ObjectStorageError::BodyTooLarge {
|
||||||
limit: request.max_response_body_bytes,
|
limit: request.max_response_body_bytes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut body = Vec::new();
|
body.extend_from_slice(&chunk);
|
||||||
while let Some(chunk) = response.chunk().await.map_err(ObjectStorageError::HttpRequest)? {
|
}
|
||||||
if body.len() + chunk.len() > request.max_response_body_bytes {
|
Ok(StorageHttpResponse { status, headers, body })
|
||||||
return Err(ObjectStorageError::BodyTooLarge {
|
|
||||||
limit: request.max_response_body_bytes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
body.extend_from_slice(&chunk);
|
|
||||||
}
|
|
||||||
Ok(StorageHttpResponse { status, headers, body })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +694,11 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reqwest_storage_http_client_builds_with_webpki_roots() {
|
||||||
|
ReqwestStorageHttpClient::new(Some(1_000)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn metadata_from_headers_uses_s3_defaults_and_checksum() {
|
fn metadata_from_headers_uses_s3_defaults_and_checksum() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user