feat(server): improve client builder

This commit is contained in:
DarkSky
2026-07-01 23:27:20 +08:00
parent 8ebdb7452f
commit 8c68319094
3 changed files with 60 additions and 43 deletions

2
Cargo.lock generated
View File

@@ -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",
] ]

View File

@@ -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]

View File

@@ -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();