diff --git a/src/erlcloud_ec2_meta.erl b/src/erlcloud_ec2_meta.erl index a9d8d3e3..32eff974 100644 --- a/src/erlcloud_ec2_meta.erl +++ b/src/erlcloud_ec2_meta.erl @@ -3,11 +3,15 @@ -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). --export([generate_session_token/1, generate_session_token/2, - get_instance_metadata/0, get_instance_metadata/1, get_instance_metadata/2, get_instance_metadata/3, - get_instance_user_data/0, get_instance_user_data/1, get_instance_user_data/2, - get_instance_dynamic_data/0, get_instance_dynamic_data/1, get_instance_dynamic_data/2, get_instance_dynamic_data/3]). +-export([get_instance_metadata/0, get_instance_metadata/1, get_instance_metadata/2, + get_instance_user_data/0, get_instance_user_data/1, + get_instance_dynamic_data/0, get_instance_dynamic_data/1, get_instance_dynamic_data/2, + get_imds_token/1]). +-define(IMDS_BASE_URL, "http://169.254.169.254"). +-define(IMDS_TOKEN_TTL, 21600). % 6 hours in seconds +-define(IMDS_TOKEN_REFRESH_THRESHOLD, 10800). % 3 hours in seconds +-define(ERLCLOUD_TOKEN_CACHE_KEY, {erlcloud_ec2_meta, imds_token}). -spec generate_session_token(DurationSecs :: non_neg_integer()) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. generate_session_token(DurationSecs) -> @@ -51,12 +55,17 @@ get_instance_metadata(ItemPath, Config) -> %% defaults to 169.254.169.254 %% %% +get_instance_metadata(ItemPath, Config) -> + MetaDataPath = ?IMDS_BASE_URL ++ "/latest/meta-data/" ++ ItemPath, + Headers = get_imds_headers(Config), + Timeout = erlcloud_aws:get_timeout(Config), + erlcloud_aws:http_body(erlcloud_httpc:request(MetaDataPath, get, Headers, <<>>, Timeout, Config)). + get_instance_metadata(ItemPath, Config, Token) -> MetaDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/meta-data/" ++ ItemPath, Header = maybe_token_header(Token), erlcloud_aws:http_body(erlcloud_httpc:request(MetaDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). - -spec get_instance_user_data() -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. get_instance_user_data() -> get_instance_user_data(erlcloud_aws:default_config()). @@ -75,12 +84,17 @@ get_instance_user_data(Config) -> %% defaults to 169.254.169.254 %% %% +get_instance_user_data(Config) -> + UserDataPath = ?IMDS_BASE_URL ++ "/latest/user-data/", + Headers = get_imds_headers(Config), + Timeout = erlcloud_aws:get_timeout(Config), + erlcloud_aws:http_body(erlcloud_httpc:request(UserDataPath, get, Headers, <<>>, Timeout, Config)). + get_instance_user_data(Config, Token) -> UserDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/user-data/", Header = maybe_token_header(Token), erlcloud_aws:http_body(erlcloud_httpc:request(UserDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). - -spec get_instance_dynamic_data() -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. get_instance_dynamic_data() -> get_instance_dynamic_data(erlcloud_aws:default_config()). @@ -96,12 +110,73 @@ get_instance_dynamic_data(ItemPath, Config) -> %%%--------------------------------------------------------------------------- -spec get_instance_dynamic_data( ItemPath :: string(), Config :: aws_config(), Token :: undefined | binary() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. %%%--------------------------------------------------------------------------- +get_instance_dynamic_data(ItemPath, Config) -> + DynamicDataPath = ?IMDS_BASE_URL ++ "/latest/dynamic/" ++ ItemPath, + Headers = get_imds_headers(Config), + Timeout = erlcloud_aws:get_timeout(Config), + erlcloud_aws:http_body(erlcloud_httpc:request(DynamicDataPath, get, Headers, <<>>, Timeout, Config)). +%%%--------------------------------------------------------------------------- +-spec get_instance_dynamic_data( ItemPath :: string(), Config :: aws_config(), Token :: undefined | binary() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +%%%--------------------------------------------------------------------------- get_instance_dynamic_data(ItemPath, Config, Token) -> DynamicDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/dynamic/" ++ ItemPath, Header = maybe_token_header(Token), erlcloud_aws:http_body(erlcloud_httpc:request(DynamicDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). +%%%--------------------------------------------------------------------------- +%% @doc Get or refresh IMDSv2 token from metadata service +%% Token is cached in application environment with timestamp +%% Refreshes if token is older than 3 hours to prevent expiration +%%%--------------------------------------------------------------------------- +-spec get_imds_token(aws_config()) -> {ok, string()} | {error, any()}. +get_imds_token(Config) -> + case application:get_env(erlcloud, ?ERLCLOUD_TOKEN_CACHE_KEY) of + {ok, {Timestamp, Token}} -> + CurrentTime = erlang:system_time(second), + Age = CurrentTime - Timestamp, + if + Age < ?IMDS_TOKEN_REFRESH_THRESHOLD -> + {ok, Token}; + true -> + fetch_new_imds_token(Config) + end; + undefined -> + fetch_new_imds_token(Config) + end. + +%%%--------------------------------------------------------------------------- +%% @doc Fetch a new IMDSv2 token and cache it +%%%--------------------------------------------------------------------------- +-spec fetch_new_imds_token(aws_config()) -> {ok, string()} | {error, any()}. +fetch_new_imds_token(Config) -> + URL = ?IMDS_BASE_URL ++ "/latest/api/token", + Headers = [{"X-aws-ec2-metadata-token-ttl-seconds", + integer_to_list(?IMDS_TOKEN_TTL)}], + Timeout = erlcloud_aws:get_timeout(Config), + case erlcloud_httpc:request(URL, put, Headers, <<>>, Timeout, Config) of + {ok, {{200, _}, _, Body}} -> + Token = binary_to_list(Body), + CurrentTime = erlang:system_time(second), + application:set_env(erlcloud, ?ERLCLOUD_TOKEN_CACHE_KEY, {CurrentTime, Token}), + {ok, Token}; + _ -> + {error, no_token} + end. + +%%%--------------------------------------------------------------------------- +%% @doc Get headers for IMDS requests with IMDSv2 token if available +%% Falls back to empty headers (IMDSv1) if token fetch fails +%%%--------------------------------------------------------------------------- +-spec get_imds_headers(aws_config()) -> [{string(), string()}]. +get_imds_headers(Config) -> + case get_imds_token(Config) of + {ok, Token} -> + [{"X-aws-ec2-metadata-token", Token}]; + {error, _} -> + [] % Fallback to IMDSv1 + end. + %%%------------------------------------------------------------------------------ %%% Internal functions. %%%------------------------------------------------------------------------------