diff --git a/Cargo.lock b/Cargo.lock index 367e9b5dbf..5d63b12f4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,7 @@ dependencies = [ "rand 0.9.4", "rayon", "reqwest", + "rustls", "rusty-s3", "safefetch", "schemars", @@ -239,6 +240,7 @@ dependencies = [ "url", "uuid", "v_htmlescape", + "webpki-roots 1.0.6", "y-octo", ] diff --git a/packages/backend/native/Cargo.toml b/packages/backend/native/Cargo.toml index 8aacf54ca1..b45056268a 100644 --- a/packages/backend/native/Cargo.toml +++ b/packages/backend/native/Cargo.toml @@ -42,6 +42,11 @@ rand = { workspace = true } reqwest = { version = "0.13.4", default-features = false, features = [ "rustls", ] } +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "std", + "tls12", +] } rusty-s3 = "0.10.0" safefetch = { workspace = true } schemars = { workspace = true } @@ -63,6 +68,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } url = { workspace = true } uuid = { workspace = true, features = ["v4"] } v_htmlescape = { workspace = true } +webpki-roots = "1.0" y-octo = { workspace = true, features = ["large_refs"] } [target.'cfg(not(target_os = "linux"))'.dependencies] diff --git a/packages/backend/native/src/runtime/storage_runtime/object_storage/client.rs b/packages/backend/native/src/runtime/storage_runtime/object_storage/client.rs index b225380663..cacfc3d439 100644 --- a/packages/backend/native/src/runtime/storage_runtime/object_storage/client.rs +++ b/packages/backend/native/src/runtime/storage_runtime/object_storage/client.rs @@ -1,7 +1,5 @@ use std::{ collections::HashMap, - future::Future, - pin::Pin, time::{Duration, SystemTime}, }; @@ -10,6 +8,7 @@ use reqwest::{ Client as ReqwestClient, Method, StatusCode, header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, HeaderMap, HeaderName, HeaderValue, LAST_MODIFIED}, }; +use rustls::RootCertStore; use rusty_s3::{ Bucket, Credentials, actions::{ @@ -31,8 +30,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000; const MAX_MULTIPART_PART_NUMBER: i32 = 10_000; const MAX_RESPONSE_BODY_BYTES: usize = i32::MAX as usize; -type StorageHttpFuture<'a> = Pin> + Send + 'a>>; - #[derive(Clone)] struct StorageHttpRequest { method: Method, @@ -48,10 +45,6 @@ struct StorageHttpResponse { body: Vec, } -trait StorageHttpClient: Clone + Send + Sync + 'static { - fn execute(&self, request: StorageHttpRequest) -> StorageHttpFuture<'_>; -} - #[derive(Clone)] struct ReqwestStorageHttpClient { client: ReqwestClient, @@ -59,50 +52,61 @@ struct ReqwestStorageHttpClient { impl ReqwestStorageHttpClient { fn new(request_timeout_ms: Option) -> ObjectStorageResult { - let builder = ReqwestClient::builder().timeout(Duration::from_millis( - request_timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS), - )); + let builder = ReqwestClient::builder() + .tls_backend_preconfigured(Self::webpki_tls_config()?) + .timeout(Duration::from_millis( + request_timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS), + )); Ok(Self { client: builder.build().map_err(ObjectStorageError::HttpClientBuild)?, }) } -} -impl StorageHttpClient for ReqwestStorageHttpClient { - fn execute(&self, request: StorageHttpRequest) -> StorageHttpFuture<'_> { - Box::pin(async move { - let mut builder = self.client.request(request.method, request.url); - for (key, value) in request.headers { - let name = - HeaderName::from_bytes(key.as_bytes()).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?; - 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) - { + fn webpki_tls_config() -> ObjectStorageResult { + let roots = RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + Ok( + rustls::ClientConfig::builder_with_provider(rustls::crypto::aws_lc_rs::default_provider().into()) + .with_safe_default_protocol_versions() + .map_err(|err| ObjectStorageError::Config(format!("ObjectStorage TLS config failed: {err}")))? + .with_root_certificates(roots) + .with_no_client_auth(), + ) + } + + async fn execute(&self, request: StorageHttpRequest) -> ObjectStorageResult { + let mut builder = self.client.request(request.method, request.url); + for (key, value) in request.headers { + let name = + HeaderName::from_bytes(key.as_bytes()).map_err(|err| ObjectStorageError::InvalidHeader(err.to_string()))?; + 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 { 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 { - limit: request.max_response_body_bytes, - }); - } - body.extend_from_slice(&chunk); - } - Ok(StorageHttpResponse { status, headers, body }) - }) + body.extend_from_slice(&chunk); + } + Ok(StorageHttpResponse { status, headers, body }) } } @@ -690,6 +694,11 @@ mod tests { use super::*; + #[test] + fn reqwest_storage_http_client_builds_with_webpki_roots() { + ReqwestStorageHttpClient::new(Some(1_000)).unwrap(); + } + #[test] fn metadata_from_headers_uses_s3_defaults_and_checksum() { let mut headers = HeaderMap::new();