use super::BedrockProvider;
use super::auth::BedrockAuth;
use anyhow::{Context, Result};
use hmac::{Hmac, Mac};
use reqwest::Url;
use sha2::{Digest, Sha256};
impl BedrockProvider {
pub(super) fn base_url(&self) -> String {
format!("https://bedrock-runtime.{}.amazonaws.com", self.region)
}
pub(super) fn management_url(&self) -> String {
format!("https://bedrock.{}.amazonaws.com", self.region)
}
pub async fn send_converse_request(&self, url: &str, body: &[u8]) -> Result<reqwest::Response> {
self.send_request("POST", url, Some(body), "bedrock").await
}
pub(super) async fn send_request(
&self,
method: &str,
url: &str,
body: Option<&[u8]>,
service: &str,
) -> Result<reqwest::Response> {
match &self.auth {
BedrockAuth::SigV4(_) => {
self.send_signed_request(method, url, body.unwrap_or(b""), service)
.await
}
BedrockAuth::BearerToken(token) => {
let mut req = self
.client
.request(method.parse().unwrap_or(reqwest::Method::GET), url)
.bearer_auth(token)
.header("content-type", "application/json")
.header("accept", "application/json");
if let Some(b) = body {
req = req.body(b.to_vec());
}
req.send()
.await
.context("Failed to send request to Bedrock")
}
}
}
async fn send_signed_request(
&self,
method: &str,
url: &str,
body: &[u8],
service: &str,
) -> Result<reqwest::Response> {
let creds = match &self.auth {
BedrockAuth::SigV4(c) => c,
BedrockAuth::BearerToken(_) => {
anyhow::bail!("send_signed_request called with bearer token auth");
}
};
let now = chrono::Utc::now();
let datestamp = now.format("%Y%m%d").to_string();
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
let canonical_url = canonicalize_url(url)?;
let host = canonical_url.host;
let canonical_uri = canonical_url.canonical_uri;
let canonical_querystring = canonical_url.canonical_querystring;
let payload_hash = sha256_hex(body);
let mut headers_map: Vec<(&str, String)> = vec![
("content-type", "application/json".to_string()),
("host", host.clone()),
("x-amz-content-sha256", payload_hash.clone()),
("x-amz-date", amz_date.clone()),
];
if let Some(token) = &creds.session_token {
headers_map.push(("x-amz-security-token", token.clone()));
}
headers_map.sort_by_key(|(k, _)| *k);
let canonical_headers: String = headers_map
.iter()
.map(|(k, v)| format!("{k}:{v}"))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
let signed_headers: String = headers_map
.iter()
.map(|(k, _)| *k)
.collect::<Vec<_>>()
.join(";");
let canonical_request = format!(
"{method}\n{canonical_uri}\n{canonical_querystring}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
);
let credential_scope = format!("{datestamp}/{}/{service}/aws4_request", self.region);
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{}",
sha256_hex(canonical_request.as_bytes())
);
let k_date = hmac_sha256(
format!("AWS4{}", creds.secret_access_key).as_bytes(),
datestamp.as_bytes(),
);
let k_region = hmac_sha256(&k_date, self.region.as_bytes());
let k_service = hmac_sha256(&k_region, service.as_bytes());
let k_signing = hmac_sha256(&k_service, b"aws4_request");
let signature = hex::encode(hmac_sha256(&k_signing, string_to_sign.as_bytes()));
let authorization = format!(
"AWS4-HMAC-SHA256 Credential={}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}",
creds.access_key_id
);
let mut req = self
.client
.request(method.parse().unwrap_or(reqwest::Method::POST), url)
.header("content-type", "application/json")
.header("host", &host)
.header("x-amz-date", &amz_date)
.header("x-amz-content-sha256", &payload_hash)
.header("authorization", &authorization);
if let Some(token) = &creds.session_token {
req = req.header("x-amz-security-token", token);
}
if method == "POST" || method == "PUT" {
req = req.body(body.to_vec());
}
req.send()
.await
.context("Failed to send signed request to Bedrock")
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct CanonicalUrl {
pub(super) host: String,
pub(super) canonical_uri: String,
pub(super) canonical_querystring: String,
}
pub(super) fn canonicalize_url(url: &str) -> Result<CanonicalUrl> {
let parsed = Url::parse(url).with_context(|| format!("Invalid Bedrock URL: {url}"))?;
let host = canonical_host(&parsed).context("Bedrock URL missing host")?;
let canonical_uri = canonical_uri(&parsed)?;
let canonical_querystring = canonical_querystring(&parsed);
Ok(CanonicalUrl {
host,
canonical_uri,
canonical_querystring,
})
}
fn canonical_host(url: &Url) -> Option<String> {
let host = url.host_str()?.to_string();
Some(match url.port() {
Some(port) => format!("{host}:{port}"),
None => host,
})
}
fn canonical_uri(url: &Url) -> Result<String> {
let segments = url.path_segments().context("Bedrock URL missing path")?;
let encoded = segments
.map(canonical_path_segment)
.collect::<Result<Vec<_>>>()?;
Ok(format!("/{}", encoded.join("/")))
}
fn canonical_path_segment(segment: &str) -> Result<String> {
let decoded = urlencoding::decode(segment)
.with_context(|| format!("invalid percent-encoding in path segment `{segment}`"))?;
Ok(urlencoding::encode(&decoded).into_owned())
}
fn canonical_querystring(url: &Url) -> String {
let mut pairs = url
.query_pairs()
.map(|(key, value)| {
(
urlencoding::encode(&key).into_owned(),
urlencoding::encode(&value).into_owned(),
)
})
.collect::<Vec<_>>();
pairs.sort();
pairs
.into_iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join("&")
}
fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}