Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/common_s3_utils.rs
Line
Count
Source
1
// Copyright 2025 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::future::Future;
16
use core::pin::Pin;
17
use core::task::{Context, Poll};
18
use std::sync::Arc;
19
20
use aws_config::retry::ErrorKind::TransientError;
21
use aws_smithy_runtime_api::client::http::{
22
    HttpClient as SmithyHttpClient, HttpConnector as SmithyHttpConnector, HttpConnectorFuture,
23
    HttpConnectorSettings, SharedHttpConnector,
24
};
25
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
26
use aws_smithy_runtime_api::client::result::ConnectorError;
27
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
28
use aws_smithy_runtime_api::http::Response;
29
use aws_smithy_types::body::SdkBody;
30
use bytes::{Bytes, BytesMut};
31
use futures::Stream;
32
use http_body::{Frame, SizeHint};
33
use http_body_util::BodyExt;
34
use hyper::{Method, Request};
35
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
36
use hyper_util::client::legacy::Client as LegacyClient;
37
use hyper_util::client::legacy::connect::HttpConnector as LegacyHttpConnector;
38
use hyper_util::rt::TokioExecutor;
39
use nativelink_config::stores::CommonObjectSpec;
40
// Note: S3 store should be very careful about the error codes it returns
41
// when in a retryable wrapper. Always prefer Code::Aborted or another
42
// retryable code over Code::InvalidArgument or make_input_err!().
43
// ie: Don't import make_input_err!() to help prevent this.
44
use nativelink_error::{Code, make_err};
45
use nativelink_util::buf_channel::DropCloserReadHalf;
46
use nativelink_util::fs;
47
use nativelink_util::retry::{Retrier, RetryResult};
48
use tokio::time::sleep;
49
50
33
pub(crate) fn install_default_rustls_crypto_provider() {
51
33
    drop(rustls::crypto::ring::default_provider().install_default());
52
33
}
53
54
#[derive(Clone)]
55
pub struct TlsClient {
56
    client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>,
57
    retrier: Retrier,
58
}
59
60
impl TlsClient {
61
    #[must_use]
62
3
    pub fn new(common: &CommonObjectSpec) -> Self {
63
3
        install_default_rustls_crypto_provider();
64
65
3
        let connector_with_roots = HttpsConnectorBuilder::new().with_platform_verifier();
66
67
3
        let connector_with_schemes = if common.insecure_allow_http {
68
0
            connector_with_roots.https_or_http()
69
        } else {
70
3
            connector_with_roots.https_only()
71
        };
72
73
3
        let connector = if common.disable_http2 {
74
0
            connector_with_schemes.enable_http1().build()
75
        } else {
76
3
            connector_with_schemes.enable_http1().enable_http2().build()
77
        };
78
79
3
        Self::with_https_connector(common, connector)
80
3
    }
81
82
6
    pub fn with_https_connector(
83
6
        common: &CommonObjectSpec,
84
6
        connector: HttpsConnector<LegacyHttpConnector>,
85
6
    ) -> Self {
86
6
        install_default_rustls_crypto_provider();
87
88
6
        let client = LegacyClient::builder(TokioExecutor::new()).build(connector);
89
90
        Self {
91
6
            client,
92
6
            retrier: Retrier::new(
93
6
                Arc::new(|duration| 
Box::pin0
(
sleep0
(
duration0
))),
94
6
                common.retry.make_jitter_fn(),
95
6
                common.retry.clone(),
96
            ),
97
        }
98
6
    }
99
}
100
101
impl core::fmt::Debug for TlsClient {
102
42
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
103
42
        f.debug_struct("TlsClient").finish_non_exhaustive()
104
42
    }
105
}
106
107
impl SmithyHttpClient for TlsClient {
108
6
    fn http_connector(
109
6
        &self,
110
6
        _settings: &HttpConnectorSettings,
111
6
        _components: &RuntimeComponents,
112
6
    ) -> SharedHttpConnector {
113
6
        SharedHttpConnector::new(self.clone())
114
6
    }
115
}
116
117
struct RequestBuilder<'a> {
118
    components: &'a RequestComponents,
119
}
120
121
impl<'a> RequestBuilder<'a> {
122
    #[inline]
123
6
    const fn new(components: &'a RequestComponents) -> Self {
124
6
        Self { components }
125
6
    }
126
127
    #[inline]
128
    #[allow(unused_qualifications, reason = "false positive on hyper::http::Error")]
129
6
    fn build(&self) -> Result<Request<SdkBody>, hyper::http::Error> {
130
6
        let mut req_builder = Request::builder()
131
6
            .method(self.components.method.clone())
132
6
            .uri(self.components.uri.clone())
133
6
            .version(self.components.version);
134
135
6
        let headers_map = req_builder.headers_mut().unwrap();
136
18
        for (name, value) in 
&self.components.headers6
{
137
18
            headers_map.insert(name, value.clone());
138
18
        }
139
140
6
        match &self.components.body_data {
141
6
            BufferedBodyState::Cloneable(body) => {
142
6
                let cloned_body = body.try_clone().expect("Body should be cloneable");
143
6
                req_builder.body(cloned_body)
144
            }
145
0
            BufferedBodyState::Buffered(bytes) => req_builder.body(SdkBody::from(bytes.clone())),
146
0
            BufferedBodyState::Empty => req_builder.body(SdkBody::empty()),
147
        }
148
6
    }
149
}
150
151
mod execution {
152
    use super::conversions::ResponseExt;
153
    use super::{
154
        Code, HttpsConnector, LegacyClient, LegacyHttpConnector, RequestBuilder, RequestComponents,
155
        Response, RetryResult, SdkBody, fs, make_err,
156
    };
157
158
    #[inline]
159
6
    pub(crate) async fn execute_request(
160
6
        client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>,
161
6
        components: &RequestComponents,
162
6
    ) -> RetryResult<Response<SdkBody>> {
163
6
        let _permit = match fs::get_permit().await {
164
6
            Ok(permit) => permit,
165
0
            Err(e) => {
166
0
                return RetryResult::Retry(make_err!(
167
0
                    Code::Unavailable,
168
0
                    "Failed to acquire permit: {e}"
169
0
                ));
170
            }
171
        };
172
173
6
        let request = match RequestBuilder::new(components).build() {
174
6
            Ok(req) => req,
175
0
            Err(e) => {
176
0
                return RetryResult::Err(make_err!(
177
0
                    Code::Internal,
178
0
                    "Failed to create request: {e}",
179
0
                ));
180
            }
181
        };
182
183
6
        match client.request(request).await {
184
0
            Ok(resp) => RetryResult::Ok(resp.into_smithy_response()),
185
6
            Err(e) => RetryResult::Retry(make_err!(
186
6
                Code::Unavailable,
187
6
                "Failed request in S3Store: {e}"
188
6
            )),
189
        }
190
6
    }
191
192
    #[inline]
193
6
    pub(crate) fn create_retry_stream(
194
6
        client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>,
195
6
        components: RequestComponents,
196
6
    ) -> impl futures::Stream<Item = RetryResult<Response<SdkBody>>> {
197
6
        futures::stream::unfold(components, move |components| {
198
6
            let client_clone = client.clone();
199
6
            async move {
200
6
                let result = execute_request(client_clone, &components).await;
201
202
6
                Some((result, components))
203
6
            }
204
6
        })
205
6
    }
206
}
207
208
enum BufferedBodyState {
209
    Cloneable(SdkBody),
210
    Buffered(Bytes),
211
    Empty,
212
}
213
214
mod body_processing {
215
    use super::{BodyExt, BufferedBodyState, BytesMut, ConnectorError, SdkBody, TransientError};
216
217
    /// Buffer a request body fully into memory.
218
    ///
219
    /// TODO(aaronmondal): This could lead to OOMs in extremely constrained
220
    ///                    environments. Probably better to implement something
221
    ///                    like a rewindable stream logic.
222
    #[inline]
223
0
    pub(crate) async fn buffer_body(body: SdkBody) -> Result<BufferedBodyState, ConnectorError> {
224
0
        let mut bytes = BytesMut::new();
225
0
        let mut body_stream = body;
226
0
        while let Some(frame) = body_stream.frame().await {
227
0
            match frame {
228
0
                Ok(frame) => {
229
0
                    if let Some(data) = frame.data_ref() {
230
0
                        bytes.extend_from_slice(data);
231
0
                    }
232
                }
233
0
                Err(e) => {
234
0
                    return Err(ConnectorError::other(
235
0
                        format!("Failed to read request body: {e}").into(),
236
0
                        Some(TransientError),
237
0
                    ));
238
                }
239
            }
240
        }
241
242
0
        Ok(BufferedBodyState::Buffered(bytes.freeze()))
243
0
    }
244
}
245
246
pub(crate) struct RequestComponents {
247
    method: Method,
248
    uri: hyper::Uri,
249
    version: hyper::Version,
250
    headers: hyper::HeaderMap,
251
    body_data: BufferedBodyState,
252
}
253
254
mod conversions {
255
    use super::{
256
        BufferedBodyState, ConnectorError, Future, HttpRequest, Method, RequestComponents,
257
        Response, SdkBody, TransientError, body_processing,
258
    };
259
260
    pub(crate) trait RequestExt {
261
        fn into_components(self)
262
        -> impl Future<Output = Result<RequestComponents, ConnectorError>>;
263
    }
264
265
    impl RequestExt for HttpRequest {
266
6
        async fn into_components(self) -> Result<RequestComponents, ConnectorError> {
267
            // Note: This does *not* refer the the HTTP protocol, but to the
268
            //       version of the http crate.
269
6
            let hyper_req = self.try_into_http1x().map_err(|e| 
{0
270
0
                ConnectorError::other(
271
0
                    format!("Failed to convert to HTTP request: {e}").into(),
272
0
                    Some(TransientError),
273
                )
274
0
            })?;
275
276
6
            let method = hyper_req.method().clone();
277
6
            let uri = hyper_req.uri().clone();
278
6
            let version = hyper_req.version();
279
6
            let headers = hyper_req.headers().clone();
280
281
6
            let body = hyper_req.into_body();
282
283
            // Only buffer bodies for methods likely to have payloads.
284
6
            let needs_buffering = 
matches!0
(method, Method::POST | Method::PUT);
285
286
            // Preserve the body in case we need to retry.
287
6
            let body_data = if needs_buffering {
288
6
                if let Some(cloneable_body) = body.try_clone() {
289
6
                    BufferedBodyState::Cloneable(cloneable_body)
290
                } else {
291
0
                    body_processing::buffer_body(body).await?
292
                }
293
            } else {
294
0
                BufferedBodyState::Empty
295
            };
296
297
6
            Ok(RequestComponents {
298
6
                method,
299
6
                uri,
300
6
                version,
301
6
                headers,
302
6
                body_data,
303
6
            })
304
6
        }
305
    }
306
307
    pub(crate) trait ResponseExt {
308
        fn into_smithy_response(self) -> Response<SdkBody>;
309
    }
310
311
    impl ResponseExt for hyper::Response<hyper::body::Incoming> {
312
0
        fn into_smithy_response(self) -> Response<SdkBody> {
313
0
            let (parts, body) = self.into_parts();
314
0
            let sdk_body = SdkBody::from_body_1_x(body);
315
0
            let mut smithy_resp = Response::new(parts.status.into(), sdk_body);
316
0
            let header_pairs: Vec<(String, String)> = parts
317
0
                .headers
318
0
                .iter()
319
0
                .filter_map(|(name, value)| {
320
0
                    value
321
0
                        .to_str()
322
0
                        .ok()
323
0
                        .map(|value_str| (name.as_str().to_owned(), value_str.to_owned()))
324
0
                })
325
0
                .collect();
326
327
0
            for (name, value) in header_pairs {
328
0
                smithy_resp.headers_mut().insert(name, value);
329
0
            }
330
331
0
            smithy_resp
332
0
        }
333
    }
334
}
335
336
impl SmithyHttpConnector for TlsClient {
337
6
    fn call(&self, req: HttpRequest) -> HttpConnectorFuture {
338
        use conversions::RequestExt;
339
340
6
        let client = self.client.clone();
341
6
        let retrier = self.retrier.clone();
342
343
6
        HttpConnectorFuture::new(Box::pin(async move {
344
6
            let components = req.into_components().await
?0
;
345
346
6
            let retry_stream = execution::create_retry_stream(client, components);
347
348
6
            match retrier.retry(retry_stream).await {
349
0
                Ok(response) => Ok(response),
350
6
                Err(e) => Err(ConnectorError::other(
351
6
                    format!("Connection failed after retries: {e}").into(),
352
6
                    Some(TransientError),
353
6
                )),
354
            }
355
6
        }))
356
6
    }
357
}
358
359
#[derive(Debug)]
360
pub struct BodyWrapper {
361
    pub reader: DropCloserReadHalf,
362
    pub size: u64,
363
}
364
365
impl http_body::Body for BodyWrapper {
366
    type Data = Bytes;
367
    type Error = std::io::Error;
368
369
83
    fn poll_frame(
370
83
        self: Pin<&mut Self>,
371
83
        cx: &mut Context<'_>,
372
83
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
373
83
        let reader = Pin::new(&mut Pin::get_mut(self).reader);
374
83
        reader
375
83
            .poll_next(cx)
376
83
            .map(|maybe_bytes_res| 
maybe_bytes_res58
.
map58
(|res|
res50
.
map50
(Frame::data)))
377
83
    }
378
379
4
    fn size_hint(&self) -> SizeHint {
380
4
        SizeHint::with_exact(self.size)
381
4
    }
382
}