use std::time::SystemTime;
use serde_json::Value as JsonValue;
use vantage_core::{Result, error};
use crate::account::AwsAccount;
use crate::sign::sign_v4;
pub(crate) async fn restjson_call(
account: &AwsAccount,
service: &str,
method: &str,
path: &str,
query: &[(String, String)],
) -> Result<JsonValue> {
let region = account.region();
if region.is_empty() {
return Err(error!(
"AWS region is not configured — pass it to AwsAccount::new \
or set AWS_REGION before issuing a REST-JSON request"
));
}
let host = format!("{service}.{region}.amazonaws.com");
let url = build_url(&host, path, query);
let body_bytes: Vec<u8> = Vec::new();
let signing_headers = [("host".to_string(), host.clone())];
let signed = sign_v4(
account.access_key(),
account.secret_key(),
account.session_token(),
region,
service,
method,
&url,
&signing_headers,
&body_bytes,
SystemTime::now(),
)?;
let req_builder = match method {
"GET" => account.http().get(&url),
"HEAD" => account.http().head(&url),
other => {
return Err(error!(
"REST-JSON transport currently only supports read methods",
method = other
));
}
};
let mut req = req_builder;
for h in &signed {
req = req.header(h.name.as_str(), h.value.as_str());
}
let resp = req.send().await.map_err(|e| {
error!(
"AWS REST-JSON request failed",
url = url.as_str(),
method = method,
detail = e
)
})?;
let status = resp.status();
let response_text = resp
.text()
.await
.map_err(|e| error!("Failed to read AWS REST-JSON response body", detail = e))?;
if !status.is_success() {
return Err(error!(
"AWS REST-JSON request returned error status",
url = url.as_str(),
status = status.as_u16(),
body = response_text
));
}
serde_json::from_str(&response_text).map_err(|e| {
error!(
"Failed to parse AWS REST-JSON response",
detail = e,
body_preview = response_text.chars().take(200).collect::<String>()
)
})
}
fn build_url(host: &str, path: &str, query: &[(String, String)]) -> String {
let mut url = format!("https://{host}{path}");
if !query.is_empty() {
url.push('?');
for (i, (k, v)) in query.iter().enumerate() {
if i > 0 {
url.push('&');
}
url.push_str(&query_part_encode(k));
url.push('=');
url.push_str(&query_part_encode(v));
}
}
url
}
fn query_part_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
let unreserved = b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~');
if unreserved {
out.push(b as char);
} else {
out.push_str(&format!("%{b:02X}"));
}
}
out
}