use crate::error::Error;
use crate::source::{Probe, Source, SourceKind};
use crate::sources::util::{normalize, trim_trailing_slashes};
use crate::transport::HttpTransport;
const DEFAULT_BASE_URL: &str = "http://169.254.169.254";
const TOKEN_PATH: &str = "/latest/api/token";
const DOCUMENT_PATH: &str = "/latest/dynamic/instance-identity/document";
const TOKEN_TTL_HEADER: &str = "X-aws-ec2-metadata-token-ttl-seconds";
const TOKEN_HEADER: &str = "X-aws-ec2-metadata-token";
const TOKEN_TTL_SECONDS: &str = "21600";
pub struct AwsImds<T> {
transport: T,
base_url: String,
}
impl<T> AwsImds<T> {
pub fn new(transport: T) -> Self {
Self::with_base_url(transport, DEFAULT_BASE_URL)
}
pub fn with_base_url(transport: T, base_url: impl Into<String>) -> Self {
Self {
transport,
base_url: trim_trailing_slashes(base_url),
}
}
}
impl<T> std::fmt::Debug for AwsImds<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AwsImds")
.field("base_url", &self.base_url)
.finish_non_exhaustive()
}
}
impl<T: HttpTransport + 'static> Source for AwsImds<T> {
fn kind(&self) -> SourceKind {
SourceKind::AwsImds
}
fn probe(&self) -> Result<Option<Probe>, Error> {
let Some(token) = fetch_token(&self.transport, &self.base_url) else {
return Ok(None);
};
let Some(document) = fetch_document(&self.transport, &self.base_url, &token) else {
return Ok(None);
};
let instance_id = extract_instance_id(&document).ok_or_else(|| Error::Platform {
source_kind: SourceKind::AwsImds,
reason: "instance-identity document missing `instanceId` field".to_owned(),
})?;
Ok(normalize(&instance_id).map(|v| Probe::new(SourceKind::AwsImds, v)))
}
}
fn fetch_token<T: HttpTransport>(transport: &T, base_url: &str) -> Option<String> {
let request = http::Request::builder()
.method(http::Method::PUT)
.uri(format!("{base_url}{TOKEN_PATH}"))
.header(TOKEN_TTL_HEADER, TOKEN_TTL_SECONDS)
.body(Vec::new())
.ok()?;
send_plaintext(transport, request, "token")
}
fn fetch_document<T: HttpTransport>(transport: &T, base_url: &str, token: &str) -> Option<String> {
let request = http::Request::builder()
.method(http::Method::GET)
.uri(format!("{base_url}{DOCUMENT_PATH}"))
.header(TOKEN_HEADER, token)
.body(Vec::new())
.ok()?;
send_plaintext(transport, request, "document")
}
fn send_plaintext<T: HttpTransport>(
transport: &T,
request: http::Request<Vec<u8>>,
label: &str,
) -> Option<String> {
let response = transport.send(request).ok()?;
if !response.status().is_success() {
log::debug!("aws-imds: {label} endpoint returned {}", response.status());
return None;
}
std::str::from_utf8(response.body()).ok().map(str::to_owned)
}
fn extract_instance_id(json: &str) -> Option<String> {
let key = "\"instanceId\"";
let mut cursor = 0;
let start = loop {
let rel = json[cursor..].find(key)?;
let abs = cursor + rel;
if is_at_top_level_boundary(json, abs) {
break abs;
}
cursor = abs + key.len();
};
let after_key = &json[start + key.len()..];
let colon = after_key.find(':')?;
let after_colon = after_key[colon + 1..].trim_start();
let after_open_quote = after_colon.strip_prefix('"')?;
let close = after_open_quote.find('"')?;
Some(after_open_quote[..close].to_owned())
}
fn is_at_top_level_boundary(json: &str, key_start: usize) -> bool {
json[..key_start]
.bytes()
.rev()
.find(|b| !b.is_ascii_whitespace())
.is_some_and(|b| b == b'{' || b == b',')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sources::cloud::test_support::{
StubTransport, ok as ok_response, status as status_response,
};
const IID_DOC: &str = r#"{
"accountId": "123456789012",
"architecture": "x86_64",
"availabilityZone": "us-east-1a",
"instanceId": "i-0abc1234def567890",
"instanceType": "t3.small",
"region": "us-east-1"
}"#;
#[test]
fn happy_path_returns_instance_id() {
let stub = StubTransport::new(vec![ok_response("AQAAA-token-bytes"), ok_response(IID_DOC)]);
let source = AwsImds::new(stub);
let probe = source.probe().unwrap().expect("should produce a probe");
assert_eq!(probe.kind(), SourceKind::AwsImds);
assert_eq!(probe.value(), "i-0abc1234def567890");
}
#[test]
fn requests_follow_imdsv2_contract() {
let (stub, transport) =
StubTransport::shared(vec![ok_response("tok"), ok_response(IID_DOC)]);
let source = AwsImds::with_base_url(transport, "http://imds.test");
assert!(source.probe().unwrap().is_some());
let requests = stub.requests();
assert_eq!(requests.len(), 2);
let (ref method, ref uri, ref headers) = requests[0];
assert_eq!(method, http::Method::PUT);
assert_eq!(uri, "http://imds.test/latest/api/token");
assert_eq!(headers.get(TOKEN_TTL_HEADER).unwrap(), TOKEN_TTL_SECONDS);
let (ref method, ref uri, ref headers) = requests[1];
assert_eq!(method, http::Method::GET);
assert_eq!(
uri,
"http://imds.test/latest/dynamic/instance-identity/document"
);
assert_eq!(headers.get(TOKEN_HEADER).unwrap(), "tok");
}
#[test]
fn token_non_2xx_returns_none() {
let stub = StubTransport::new(vec![status_response(401)]);
let source = AwsImds::new(stub);
assert!(source.probe().unwrap().is_none());
}
#[test]
fn document_non_2xx_returns_none() {
let stub = StubTransport::new(vec![ok_response("tok"), status_response(404)]);
let source = AwsImds::new(stub);
assert!(source.probe().unwrap().is_none());
}
#[test]
fn transport_error_returns_none() {
#[derive(Debug)]
struct FakeErr;
impl std::fmt::Display for FakeErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("nope")
}
}
impl std::error::Error for FakeErr {}
let transport = |_req: http::Request<Vec<u8>>| -> Result<http::Response<Vec<u8>>, FakeErr> {
Err(FakeErr)
};
let source = AwsImds::new(transport);
assert!(source.probe().unwrap().is_none());
}
#[test]
fn missing_instance_id_field_errors() {
let document = r#"{"accountId": "123", "region": "us-east-1"}"#;
let stub = StubTransport::new(vec![ok_response("tok"), ok_response(document)]);
let source = AwsImds::new(stub);
let err = source.probe().expect_err("missing field must error");
assert!(matches!(
&err,
Error::Platform { source_kind, reason }
if *source_kind == SourceKind::AwsImds && reason.contains("instanceId")
));
}
#[test]
fn extract_instance_id_parses_simple_document() {
assert_eq!(
extract_instance_id(IID_DOC).as_deref(),
Some("i-0abc1234def567890")
);
}
#[test]
fn extract_instance_id_returns_none_when_field_absent() {
assert_eq!(extract_instance_id(r#"{"region": "us-east-1"}"#), None);
}
#[test]
fn extract_instance_id_skips_key_embedded_in_string_value() {
let doc = r#"{"note": "x\"instanceId\":\"i-attacker\"", "instanceId": "i-real"}"#;
assert_eq!(extract_instance_id(doc).as_deref(), Some("i-real"));
}
#[test]
fn extract_instance_id_rejects_when_only_match_is_embedded() {
let doc = r#"{"note": "x,\"instanceId\":\"i-attacker\""}"#;
assert_eq!(extract_instance_id(doc), None);
}
#[test]
fn extract_instance_id_tolerates_whitespace_around_colon() {
let doc = r#"{"instanceId" : "i-xyz"}"#;
assert_eq!(extract_instance_id(doc).as_deref(), Some("i-xyz"));
}
}