use reqsign_core::{Error, Result};
use serde::Deserialize;
pub fn sts_endpoint(region: Option<&str>, use_regional: bool) -> Result<String> {
if use_regional {
let region =
region.ok_or_else(|| Error::config_invalid("regional STS endpoint requires region"))?;
if region.starts_with("cn-") {
Ok(format!("sts.{region}.amazonaws.com.cn"))
} else {
Ok(format!("sts.{region}.amazonaws.com"))
}
} else {
let region = region.unwrap_or_default();
if region.starts_with("cn") {
Ok("sts.amazonaws.com.cn".to_string())
} else {
Ok("sts.amazonaws.com".to_string())
}
}
}
#[derive(Debug, Deserialize)]
pub struct AwsErrorResponse {
#[serde(rename = "Error")]
pub error: AwsError,
}
#[derive(Debug, Deserialize)]
pub struct AwsError {
#[serde(rename = "Code")]
pub code: String,
#[serde(rename = "Message")]
pub message: String,
}
pub fn parse_sts_error(
operation: &str,
status: http::StatusCode,
body: &str,
request_id: Option<&str>,
) -> Error {
if let Ok(error_resp) = quick_xml::de::from_str::<AwsErrorResponse>(body) {
let code = &error_resp.error.code;
let message = &error_resp.error.message;
let mut error = match code.as_str() {
"AccessDenied" | "UnauthorizedAccess" | "Forbidden" => {
Error::permission_denied(format!("{code}: {message}"))
}
"ExpiredToken" | "TokenRefreshRequired" | "InvalidToken" => {
Error::credential_invalid(format!("token expired or invalid: {message}"))
}
"InvalidParameterValue" | "MissingParameter" | "InvalidParameterCombination" => {
Error::config_invalid(format!("invalid configuration: {message}"))
}
"Throttling" | "RequestLimitExceeded" | "TooManyRequestsException" => {
Error::rate_limited(format!("AWS API rate limit: {message}"))
}
"ServiceUnavailable" | "InternalError" | "InternalFailure" => {
Error::unexpected(format!("AWS service error: {message}")).set_retryable(true)
}
"InvalidRequest" | "MalformedQueryString" => {
Error::request_invalid(format!("invalid request: {message}"))
}
_ => Error::unexpected(format!("AWS error [{code}]: {message}")),
};
error = error
.with_context(format!("operation: {operation}"))
.with_context(format!("error_code: {code}"));
if let Some(id) = request_id {
error = error.with_context(format!("request_id: {id}"));
}
error
} else {
let mut error = match status.as_u16() {
400..=499 if status == http::StatusCode::FORBIDDEN => {
Error::permission_denied(format!("STS request forbidden: {body}"))
}
400..=499 if status == http::StatusCode::UNAUTHORIZED => {
Error::credential_invalid(format!("STS authentication failed: {body}"))
}
429 => Error::rate_limited(format!("STS rate limit exceeded: {body}")),
400..=499 => {
Error::request_invalid(format!("STS request failed with {status}: {body}"))
}
500..=599 => {
Error::unexpected(format!("STS server error {status}: {body}")).set_retryable(true)
}
_ => Error::unexpected(format!("STS request failed with {status}: {body}")),
};
error = error
.with_context(format!("operation: {operation}"))
.with_context(format!("http_status: {status}"));
if let Some(id) = request_id {
error = error.with_context(format!("request_id: {id}"));
}
error
}
}
pub fn parse_imds_error(operation: &str, status: http::StatusCode, body: &str) -> Error {
#[derive(Debug, Deserialize)]
struct ImdsError {
#[serde(rename = "Code")]
code: String,
#[serde(rename = "Message")]
message: String,
}
if let Ok(error) = serde_json::from_str::<ImdsError>(body) {
let err = match error.code.as_str() {
"AssumeRoleUnauthorizedAccess" => Error::permission_denied(format!(
"EC2 instance not authorized to assume role: {}",
error.message
))
.with_context("hint: check if the IAM role has a trust relationship with EC2"),
"InvalidUserData.Malformed" => {
Error::config_invalid(format!("malformed instance metadata: {}", error.message))
}
_ if error.code.contains("Expired") => {
Error::credential_invalid(format!("IMDS credentials expired: {}", error.message))
}
_ => Error::unexpected(format!("IMDS error [{}]: {}", error.code, error.message)),
};
err.with_context(format!("operation: {operation}"))
.with_context(format!("error_code: {}", error.code))
} else {
match status.as_u16() {
401 | 403 => Error::permission_denied(format!("IMDS access denied: {body}"))
.with_context(format!("operation: {operation}"))
.with_context("hint: check if IMDSv2 is required"),
404 => Error::config_invalid("instance metadata not found")
.with_context(format!("operation: {operation}"))
.with_context("hint: are you running on EC2?"),
500..=599 => Error::unexpected(format!("IMDS server error: {body}"))
.with_context(format!("operation: {operation}"))
.set_retryable(true),
_ => Error::unexpected(format!("IMDS request failed: {body}"))
.with_context(format!("operation: {operation}"))
.with_context(format!("http_status: {status}")),
}
}
}