1pub mod auth;
3pub mod region;
4pub mod timeout;
5
6use std::{
7 error::Error,
8 pin::Pin,
9 sync::{
10 Arc, OnceLock,
11 atomic::{AtomicUsize, Ordering},
12 },
13 task::{Context, Poll},
14 time::{Duration, Instant, SystemTime},
15};
16
17pub use auth::{AwsAuthentication, ImdsAuthentication};
18use aws_config::{
19 Region, SdkConfig, meta::region::ProvideRegion, retry::RetryConfig, timeout::TimeoutConfig,
20};
21use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider};
22use aws_sigv4::{
23 http_request::{PayloadChecksumKind, SignableBody, SignableRequest, SigningSettings},
24 sign::v4,
25};
26use aws_smithy_async::rt::sleep::TokioSleep;
27use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;
28use aws_smithy_runtime_api::client::{
29 http::{
30 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
31 },
32 identity::Identity,
33 orchestrator::{HttpRequest, HttpResponse},
34 result::SdkError,
35 runtime_components::RuntimeComponents,
36};
37use aws_smithy_types::body::SdkBody;
38use aws_types::sdk_config::SharedHttpClient;
39use bytes::Bytes;
40use http::{HeaderMap, HeaderName, header::HeaderValue};
41use http_body::{Body, combinators::BoxBody};
42use pin_project::pin_project;
43use regex::RegexSet;
44pub use region::RegionOrEndpoint;
45use snafu::Snafu;
46pub use timeout::AwsTimeout;
47
48use crate::{
49 config::ProxyConfig,
50 http::{build_proxy_connector, build_tls_connector, status},
51 internal_events::{
52 AwsBytesSent,
53 http_client::{
54 AboutToSendHttpRequest, GotHttpResponse, GotHttpWarning, HttpRequestTelemetry,
55 HttpResponseTelemetry,
56 },
57 },
58 tls::{MaybeTlsSettings, TlsConfig},
59};
60
61static RETRIABLE_CODES: OnceLock<RegexSet> = OnceLock::new();
62
63pub fn is_retriable_error<T>(error: &SdkError<T, HttpResponse>) -> bool {
65 match error {
66 SdkError::TimeoutError(_) | SdkError::DispatchFailure(_) => true,
67 SdkError::ConstructionFailure(_) => false,
68 SdkError::ResponseError(err) => check_response(err.raw()),
69 SdkError::ServiceError(err) => check_response(err.raw()),
70 _ => {
71 warn!("AWS returned unknown error, retrying request.");
72 true
73 }
74 }
75}
76
77fn check_response(res: &HttpResponse) -> bool {
78 let retry_header = res.headers().get("x-amz-retry-after").is_some();
82
83 let re = RETRIABLE_CODES.get_or_init(|| {
97 RegexSet::new(["RequestTimeout", "RequestExpired", "ThrottlingException"])
98 .expect("invalid regex")
99 });
100
101 let status = res.status();
102 let response_body = String::from_utf8_lossy(res.body().bytes().unwrap_or(&[]));
103
104 retry_header
105 || status.is_server_error()
106 || status.as_u16() == status::TOO_MANY_REQUESTS
107 || (status.is_client_error() && re.is_match(response_body.as_ref()))
108}
109
110fn connector(
114 proxy: &ProxyConfig,
115 tls_options: Option<&TlsConfig>,
116) -> crate::Result<SharedHttpClient> {
117 let tls_settings = MaybeTlsSettings::tls_client(tls_options)?;
118
119 if proxy.enabled {
120 let proxy = build_proxy_connector(tls_settings, proxy)?;
121 Ok(HyperClientBuilder::new().build(proxy))
122 } else {
123 let tls_connector = build_tls_connector(tls_settings)?;
124 Ok(HyperClientBuilder::new().build(tls_connector))
125 }
126}
127
128pub trait ClientBuilder {
130 type Client;
132
133 fn build(&self, config: &SdkConfig) -> Self::Client;
135}
136
137pub fn region_provider(
139 proxy: &ProxyConfig,
140 tls_options: Option<&TlsConfig>,
141) -> crate::Result<impl ProvideRegion + use<>> {
142 let config = aws_config::provider_config::ProviderConfig::default()
144 .with_http_client(connector(proxy, tls_options)?);
145
146 Ok(aws_config::meta::region::RegionProviderChain::first_try(
147 aws_config::environment::EnvironmentVariableRegionProvider::new(),
148 )
149 .or_else(aws_config::profile::ProfileFileRegionProvider::builder().build())
150 .or_else(
151 aws_config::imds::region::ImdsRegionProvider::builder()
152 .configure(&config)
153 .build(),
154 ))
155}
156
157async fn resolve_region(
158 proxy: &ProxyConfig,
159 tls_options: Option<&TlsConfig>,
160 region: Option<Region>,
161) -> crate::Result<Region> {
162 match region {
163 Some(region) => Ok(region),
164 None => region_provider(proxy, tls_options)?
165 .region()
166 .await
167 .ok_or_else(|| {
168 "Could not determine region from Vector configuration or default providers".into()
169 }),
170 }
171}
172
173pub async fn create_client<T>(
175 builder: &T,
176 auth: &AwsAuthentication,
177 region: Option<Region>,
178 endpoint: Option<String>,
179 proxy: &ProxyConfig,
180 tls_options: Option<&TlsConfig>,
181 timeout: Option<&AwsTimeout>,
182) -> crate::Result<T::Client>
183where
184 T: ClientBuilder,
185{
186 create_client_and_region::<T>(builder, auth, region, endpoint, proxy, tls_options, timeout)
187 .await
188 .map(|(client, _)| client)
189}
190
191pub async fn create_client_and_region<T>(
193 builder: &T,
194 auth: &AwsAuthentication,
195 region: Option<Region>,
196 endpoint: Option<String>,
197 proxy: &ProxyConfig,
198 tls_options: Option<&TlsConfig>,
199 timeout: Option<&AwsTimeout>,
200) -> crate::Result<(T::Client, Region)>
201where
202 T: ClientBuilder,
203{
204 let retry_config = RetryConfig::disabled();
205
206 let region = resolve_region(proxy, tls_options, region).await?;
209
210 let provider_config =
211 aws_config::provider_config::ProviderConfig::empty().with_region(Some(region.clone()));
212
213 let connector = connector(proxy, tls_options)?;
214
215 let connector = AwsHttpClient {
217 http: connector,
218 region: region.clone(),
219 emit_bytes_sent: true,
220 };
221
222 let mut config_builder = SdkConfig::builder()
224 .http_client(connector)
225 .sleep_impl(Arc::new(TokioSleep::new()))
226 .identity_cache(auth.credentials_cache().await?)
227 .credentials_provider(
228 auth.credentials_provider(region.clone(), proxy, tls_options)
229 .await?,
230 )
231 .region(region.clone())
232 .retry_config(retry_config.clone());
233
234 if let Some(endpoint_override) = endpoint {
235 config_builder = config_builder.endpoint_url(endpoint_override);
236 } else if let Some(endpoint_from_config) =
237 aws_config::default_provider::endpoint_url::endpoint_url_provider(&provider_config).await
238 {
239 config_builder = config_builder.endpoint_url(endpoint_from_config);
240 }
241
242 if let Some(use_fips) =
243 aws_config::default_provider::use_fips::use_fips_provider(&provider_config).await
244 {
245 config_builder = config_builder.use_fips(use_fips);
246 }
247
248 if let Some(timeout) = timeout {
249 let mut timeout_config_builder = TimeoutConfig::builder();
250
251 let operation_timeout = timeout.operation_timeout();
252 let connect_timeout = timeout.connect_timeout();
253 let read_timeout = timeout.read_timeout();
254
255 timeout_config_builder
256 .set_operation_timeout(operation_timeout.map(Duration::from_secs))
257 .set_connect_timeout(connect_timeout.map(Duration::from_secs))
258 .set_read_timeout(read_timeout.map(Duration::from_secs));
259
260 config_builder = config_builder.timeout_config(timeout_config_builder.build());
261 }
262
263 let config = config_builder.build();
264
265 Ok((T::build(builder, &config), region))
266}
267
268#[derive(Snafu, Debug)]
269enum SigningError {
270 #[snafu(display("cannot sign the request because the headers are not valid utf-8"))]
271 NotUTF8Header,
272}
273
274pub async fn sign_request(
277 service_name: &str,
278 request: &mut http::Request<Bytes>,
279 credentials_provider: &SharedCredentialsProvider,
280 region: Option<&Region>,
281 payload_checksum_sha256: bool,
282) -> crate::Result<()> {
283 let headers = request
284 .headers()
285 .iter()
286 .map(|(k, v)| {
287 Ok((
288 k.as_str(),
289 std::str::from_utf8(v.as_bytes()).map_err(|_| SigningError::NotUTF8Header)?,
290 ))
291 })
292 .collect::<Result<Vec<_>, SigningError>>()?;
293
294 let signable_request = SignableRequest::new(
295 request.method().as_str(),
296 request.uri().to_string(),
297 headers.into_iter(),
298 SignableBody::Bytes(request.body().as_ref()),
299 )?;
300
301 let credentials = credentials_provider.provide_credentials().await?;
302 let identity = Identity::new(credentials, None);
303
304 let mut signing_settings = SigningSettings::default();
305
306 if payload_checksum_sha256 {
309 signing_settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
310 }
311
312 let signing_params_builder = v4::SigningParams::builder()
313 .identity(&identity)
314 .region(region.as_ref().map(|r| r.as_ref()).unwrap_or(""))
315 .name(service_name)
316 .time(SystemTime::now())
317 .settings(signing_settings);
318
319 let signing_params = signing_params_builder
320 .build()
321 .expect("all signing params set");
322
323 let (signing_instructions, _signature) =
324 aws_sigv4::http_request::sign(signable_request, &signing_params.into())?.into_parts();
325 signing_instructions.apply_to_request_http0x(request);
326
327 Ok(())
328}
329
330#[derive(Debug)]
331struct AwsHttpClient<T> {
332 http: T,
333 region: Region,
334 emit_bytes_sent: bool,
338}
339
340impl<T> HttpClient for AwsHttpClient<T>
341where
342 T: HttpClient,
343{
344 fn http_connector(
345 &self,
346 settings: &HttpConnectorSettings,
347 components: &RuntimeComponents,
348 ) -> SharedHttpConnector {
349 let http_connector = self.http.http_connector(settings, components);
350
351 SharedHttpConnector::new(AwsConnector {
352 region: self.region.clone(),
353 http: http_connector,
354 emit_bytes_sent: self.emit_bytes_sent,
355 })
356 }
357}
358
359#[derive(Clone, Debug)]
360struct AwsConnector<T> {
361 http: T,
362 region: Region,
363 emit_bytes_sent: bool,
364}
365
366impl HttpRequestTelemetry for HttpRequest {
374 fn method(&self) -> &str {
375 self.method()
376 }
377
378 fn uri(&self) -> String {
379 self.uri().to_string()
380 }
381
382 fn headers(&self) -> HeaderMap<HeaderValue> {
383 smithy_headers_to_map(self.headers())
384 }
385
386 fn body_size_hint(&self) -> (u64, Option<u64>) {
387 let hint = Body::size_hint(self.body());
388 (hint.lower(), hint.upper())
389 }
390
391 fn extra_sensitive_headers(&self) -> &'static [HeaderName] {
392 &AWS_EXTRA_SENSITIVE_HEADERS
393 }
394}
395
396impl HttpResponseTelemetry for HttpResponse {
397 fn status_u16(&self) -> u16 {
398 self.status().as_u16()
399 }
400
401 fn headers(&self) -> HeaderMap<HeaderValue> {
402 smithy_headers_to_map(self.headers())
403 }
404
405 fn body_size_hint(&self) -> (u64, Option<u64>) {
406 let hint = Body::size_hint(self.body());
407 (hint.lower(), hint.upper())
408 }
409
410 fn extra_sensitive_headers(&self) -> &'static [HeaderName] {
411 &AWS_EXTRA_SENSITIVE_HEADERS
412 }
413}
414
415static AWS_EXTRA_SENSITIVE_HEADERS: [HeaderName; 3] = [
422 HeaderName::from_static("x-amz-security-token"),
423 HeaderName::from_static("x-amz-sso_bearer_token"),
424 HeaderName::from_static("x-aws-ec2-metadata-token"),
425];
426
427fn smithy_headers_to_map(
431 headers: &aws_smithy_runtime_api::http::Headers,
432) -> HeaderMap<HeaderValue> {
433 let mut map = HeaderMap::with_capacity(headers.len());
434 for (name, value) in headers {
435 let Ok(header_name) = http::HeaderName::from_bytes(name.as_bytes()) else {
436 continue;
437 };
438 let Ok(header_value) = HeaderValue::from_str(value) else {
439 continue;
440 };
441 map.insert(header_name, header_value);
442 }
443 map
444}
445
446impl<T> AwsConnector<T>
449where
450 T: HttpConnector,
451{
452 fn call_inner(&self, req: HttpRequest) -> HttpConnectorFuture {
455 emit!(AboutToSendHttpRequest { request: &req });
456
457 let fut: HttpConnectorFuture = self.http.call(req);
458
459 HttpConnectorFuture::new(async move {
460 let before = Instant::now();
461 let result = fut.await;
462 let roundtrip = before.elapsed();
463
464 match &result {
465 Ok(response) => {
466 emit!(GotHttpResponse {
467 response,
468 roundtrip
469 });
470 }
471 Err(error) => {
472 emit!(GotHttpWarning { error, roundtrip });
473 }
474 }
475
476 result
477 })
478 }
479}
480
481impl<T> HttpConnector for AwsConnector<T>
482where
483 T: HttpConnector,
484{
485 fn call(&self, req: HttpRequest) -> HttpConnectorFuture {
486 if !self.emit_bytes_sent {
487 return HttpConnectorFuture::new(self.call_inner(req));
488 }
489
490 let bytes_sent = Arc::new(AtomicUsize::new(0));
491
492 let req = req.map(|body| {
493 let bytes_sent = Arc::clone(&bytes_sent);
494 body.map_preserve_contents(move |body| {
495 let body = MeasuredBody::new(body, Arc::clone(&bytes_sent));
496 SdkBody::from_body_0_4(BoxBody::new(body))
497 })
498 });
499
500 let fut = self.call_inner(req);
501 let region = self.region.clone();
502
503 HttpConnectorFuture::new(async move {
504 let result = fut.await;
505
506 if let Ok(response) = &result
507 && response.status().is_success()
508 {
509 let byte_size = bytes_sent.load(Ordering::Relaxed);
510
511 emit!(AwsBytesSent {
512 byte_size,
513 region: Some(region),
514 });
515 };
516
517 result
518 })
519 }
520}
521
522#[pin_project]
523struct MeasuredBody {
524 #[pin]
525 inner: SdkBody,
526 shared_bytes_sent: Arc<AtomicUsize>,
527}
528
529impl MeasuredBody {
530 const fn new(body: SdkBody, shared_bytes_sent: Arc<AtomicUsize>) -> Self {
531 Self {
532 inner: body,
533 shared_bytes_sent,
534 }
535 }
536}
537
538impl Body for MeasuredBody {
539 type Data = Bytes;
540 type Error = Box<dyn Error + Send + Sync>;
541
542 fn poll_data(
543 self: Pin<&mut Self>,
544 cx: &mut Context<'_>,
545 ) -> Poll<Option<Result<Self::Data, Self::Error>>> {
546 let this = self.project();
547
548 match this.inner.poll_data(cx) {
549 Poll::Ready(Some(Ok(data))) => {
550 this.shared_bytes_sent
551 .fetch_add(data.len(), Ordering::Release);
552 Poll::Ready(Some(Ok(data)))
553 }
554 Poll::Ready(None) => Poll::Ready(None),
555 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
556 Poll::Pending => Poll::Pending,
557 }
558 }
559
560 fn poll_trailers(
561 self: Pin<&mut Self>,
562 _cx: &mut Context<'_>,
563 ) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
564 Poll::Ready(Ok(None))
565 }
566}