use aws_credential_types::Credentials;
use aws_sigv4::{
http_request::{sign, SignableBody, SignableRequest, SigningSettings},
sign::v4,
};
use http;
use sha2::{Digest, Sha256};
use std::time::SystemTime;
#[cfg(target_arch = "wasm32")]
use std::time::{Duration, UNIX_EPOCH};
use tracing::{debug, trace};
use alien_client_core::RequestBuilderExt;
use alien_client_core::{ErrorData, Result};
use alien_error::{Context, IntoAlienError};
use reqwest::RequestBuilder;
use serde::de::DeserializeOwned;
#[cfg(not(target_arch = "wasm32"))]
fn get_current_time() -> SystemTime {
SystemTime::now()
}
#[cfg(target_arch = "wasm32")]
fn get_current_time() -> SystemTime {
let millis = js_sys::Date::now();
let secs = (millis / 1000.0) as u64;
let nanos = ((millis % 1000.0) * 1_000_000.0) as u32;
UNIX_EPOCH + Duration::new(secs, nanos)
}
#[derive(Debug, Clone)]
pub struct AwsSignConfig {
pub service_name: String,
pub region: String,
pub credentials: Credentials,
pub signing_region: Option<String>,
}
pub trait AwsRequestSigner: Sized {
fn sign_aws_request(self, config: &AwsSignConfig) -> Result<Self>;
}
impl AwsRequestSigner for reqwest::RequestBuilder {
fn sign_aws_request(self, config: &AwsSignConfig) -> Result<Self> {
let (client, req_result) = self.build_split();
let reqwest_request =
req_result
.into_alien_error()
.context(ErrorData::RequestSignError {
message: format!(
"Unable to build reqwest::Request for {} service",
config.service_name
),
})?;
let body_bytes = reqwest_request
.body()
.and_then(|b| b.as_bytes().map(|b| b.to_vec()))
.unwrap_or_default();
debug!(
service = %config.service_name,
region = %config.region,
method = %reqwest_request.method(),
url = %reqwest_request.url(),
body_len = body_bytes.len(),
"Signing AWS request"
);
let mut http_request_builder = http::Request::builder()
.method(reqwest_request.method().as_str())
.uri(reqwest_request.url().as_str());
for (name, value) in reqwest_request.headers() {
if let Ok(value_str) = value.to_str() {
http_request_builder = http_request_builder.header(name, value_str);
}
}
let mut http_request = http_request_builder
.body(String::new())
.into_alien_error()
.context(ErrorData::RequestSignError {
message: format!(
"Unable to construct http::Request for {} service",
config.service_name
),
})?;
let identity = config.credentials.clone().into();
let signing_region = config
.signing_region
.as_ref()
.map(String::as_str)
.unwrap_or(&config.region);
let signing_time = get_current_time();
trace!(
signing_region = %signing_region,
service = %config.service_name,
"Signing parameters prepared"
);
let signing_settings = SigningSettings::default();
let signing_params = v4::SigningParams::builder()
.identity(&identity)
.region(signing_region)
.name(&config.service_name)
.time(signing_time)
.settings(signing_settings)
.build()
.into_alien_error()
.context(ErrorData::RequestSignError {
message: format!(
"Invalid signing parameters for {} service in region {}",
config.service_name, signing_region
),
})?
.into();
let signable_request = SignableRequest::new(
http_request.method().as_str(),
http_request.uri().to_string(),
http_request
.headers()
.iter()
.map(|(k, v)| (k.as_str(), v.to_str().unwrap_or(""))),
SignableBody::Bytes(&body_bytes),
)
.into_alien_error()
.context(ErrorData::RequestSignError {
message: format!(
"Unable to create signable request for {} service",
config.service_name
),
})?;
let (signing_instructions, signature) = sign(signable_request, &signing_params)
.into_alien_error()
.context(ErrorData::RequestSignError {
message: format!(
"SigV4 signature generation error for {} service",
config.service_name
),
})?
.into_parts();
trace!(signature = %signature, "SigV4 signature generated");
signing_instructions.apply_to_request_http1x(&mut http_request);
let (parts, _) = http_request.into_parts();
let mut signed_reqwest_request = client
.request(parts.method, parts.uri.to_string())
.body(body_bytes);
for (name, value) in parts.headers {
if let Some(name) = name {
signed_reqwest_request = signed_reqwest_request.header(name, value);
}
}
let signed_reqwest_request = signed_reqwest_request.build().into_alien_error().context(
ErrorData::RequestSignError {
message: format!(
"Unable to build final signed request for {} service",
config.service_name
),
},
)?;
debug!(
service = %config.service_name,
url = %signed_reqwest_request.url(),
"AWS request signed"
);
let signed_builder = reqwest::RequestBuilder::from_parts(client, signed_reqwest_request);
Ok(signed_builder)
}
}
pub trait AwsRequestBuilderExt {
fn host(self, host: &str) -> Self;
fn content_type_json(self) -> Self;
fn content_type_xml(self) -> Self;
fn content_type_form(self) -> Self;
fn content_sha256(self, body: &str) -> Self;
fn content_sha256_bytes(self, body: &[u8]) -> Self;
fn content_type_amz_json(self) -> Self;
}
impl AwsRequestBuilderExt for reqwest::RequestBuilder {
fn host(self, host: &str) -> Self {
self.header("host", host)
}
fn content_type_json(self) -> Self {
self.header("content-type", "application/json")
}
fn content_type_amz_json(self) -> Self {
self.header("content-type", "application/x-amz-json-1.1")
}
fn content_type_xml(self) -> Self {
self.header("content-type", "application/xml")
}
fn content_type_form(self) -> Self {
self.header("content-type", "application/x-www-form-urlencoded")
}
fn content_sha256(self, body: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(body.as_bytes());
let digest = hasher.finalize();
let hex_digest = hex::encode(digest);
self.header("x-amz-content-sha256", hex_digest)
}
fn content_sha256_bytes(self, body: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(body);
let digest = hasher.finalize();
let hex_digest = hex::encode(digest);
self.header("x-amz-content-sha256", hex_digest)
}
}
pub async fn sign_send_json<T: DeserializeOwned + Send + 'static>(
builder: RequestBuilder,
config: &AwsSignConfig,
) -> Result<T> {
builder
.sign_aws_request(config)?
.with_retry()
.send_json::<T>()
.await
}
pub async fn sign_send_xml<T: DeserializeOwned + Send + 'static>(
builder: RequestBuilder,
config: &AwsSignConfig,
) -> Result<T> {
builder
.sign_aws_request(config)?
.with_retry()
.send_xml::<T>()
.await
}
pub async fn sign_send_no_response(builder: RequestBuilder, config: &AwsSignConfig) -> Result<()> {
builder
.sign_aws_request(config)?
.with_retry()
.send_no_response()
.await
}