erlfmt entire plugin

This commit is contained in:
Simon Unge 2025-07-29 17:16:27 +00:00
parent 94b4a6aafd
commit a4fffbd7e0
No known key found for this signature in database
18 changed files with 3064 additions and 2440 deletions

View File

@ -57,18 +57,22 @@
-type sc_error() :: {error, Reason :: atom()}. -type sc_error() :: {error, Reason :: atom()}.
-type security_credentials() :: sc_ok() | sc_error(). -type security_credentials() :: sc_ok() | sc_error().
-record(imdsv2token, { token :: security_token() | undefined, -record(imdsv2token, {
expiration :: non_neg_integer() | undefined}). token :: security_token() | undefined,
expiration :: non_neg_integer() | undefined
}).
-type imdsv2token() :: #imdsv2token{}. -type imdsv2token() :: #imdsv2token{}.
-record(state, {access_key :: access_key() | undefined, -record(state, {
secret_access_key :: secret_access_key() | undefined, access_key :: access_key() | undefined,
expiration :: expiration() | undefined, secret_access_key :: secret_access_key() | undefined,
security_token :: security_token() | undefined, expiration :: expiration() | undefined,
region :: region() | undefined, security_token :: security_token() | undefined,
imdsv2_token:: imdsv2token() | undefined, region :: region() | undefined,
error :: atom() | string() | undefined}). imdsv2_token :: imdsv2token() | undefined,
error :: atom() | string() | undefined
}).
-type state() :: #state{}. -type state() :: #state{}.
-type scheme() :: atom(). -type scheme() :: atom().
@ -79,17 +83,16 @@
-type query_args() :: [tuple() | string()]. -type query_args() :: [tuple() | string()].
-type fragment() :: string(). -type fragment() :: string().
-type userinfo() :: {undefined | username(), -type userinfo() :: {undefined | username(), undefined | password()}.
undefined | password()}.
-type authority() :: {undefined | userinfo(), -type authority() :: {undefined | userinfo(), host(), undefined | tcp_port()}.
host(), -record(uri, {
undefined | tcp_port()}. scheme :: undefined | scheme(),
-record(uri, {scheme :: undefined | scheme(), authority :: authority(),
authority :: authority(), path :: undefined | path(),
path :: undefined | path(), query :: undefined | query_args(),
query :: undefined | query_args(), fragment :: undefined | fragment()
fragment :: undefined | fragment()}). }).
-type method() :: head | get | put | post | trace | options | delete | patch. -type method() :: head | get | put | post | trace | options | delete | patch.
-type http_version() :: string(). -type http_version() :: string().
@ -104,35 +107,40 @@
-type ssl_options() :: [ssl:tls_client_option()]. -type ssl_options() :: [ssl:tls_client_option()].
-type http_option() :: {timeout, timeout()} | -type http_option() ::
{connect_timeout, timeout()} | {timeout, timeout()}
{ssl, ssl_options()} | | {connect_timeout, timeout()}
{essl, ssl_options()} | | {ssl, ssl_options()}
{autoredirect, boolean()} | | {essl, ssl_options()}
{proxy_auth, {User :: string(), Password :: string()}} | | {autoredirect, boolean()}
{version, http_version()} | | {proxy_auth, {User :: string(), Password :: string()}}
{relaxed, boolean()} | | {version, http_version()}
{url_encode, boolean()}. | {relaxed, boolean()}
| {url_encode, boolean()}.
-type http_options() :: [http_option()]. -type http_options() :: [http_option()].
-record(request, {
-record(request, {access_key :: access_key(), access_key :: access_key(),
secret_access_key :: secret_access_key(), secret_access_key :: secret_access_key(),
security_token :: security_token(), security_token :: security_token(),
service :: string(), service :: string(),
region = "us-east-1" :: string(), region = "us-east-1" :: string(),
method = get :: method(), method = get :: method(),
headers = [] :: headers(), headers = [] :: headers(),
uri :: string(), uri :: string(),
body = "" :: body()}). body = "" :: body()
}).
-type request() :: #request{}. -type request() :: #request{}.
-type httpc_result() :: {ok, {status_line(), headers(), body()}} | -type httpc_result() ::
{ok, {status_code(), body()}} | {ok, {status_line(), headers(), body()}}
{error, term()}. | {ok, {status_code(), body()}}
| {error, term()}.
-type result_ok() :: {ok, {ResponseHeaders :: headers(), Response :: list()}}. -type result_ok() :: {ok, {ResponseHeaders :: headers(), Response :: list()}}.
-type result_error() :: {'error', Message :: reason_phrase(), {ResponseHeaders :: headers(), Response :: list()} | undefined} | -type result_error() ::
{'error', {credentials, Reason :: string()}} | {'error', Message :: reason_phrase(),
{'error', string()}. {ResponseHeaders :: headers(), Response :: list()} | undefined}
| {'error', {credentials, Reason :: string()}}
| {'error', string()}.
-type result() :: result_ok() | result_error(). -type result() :: result_ok() | result_error().

View File

@ -9,24 +9,28 @@
-behavior(gen_server). -behavior(gen_server).
%% API exports %% API exports
-export([get/2, get/3, -export([
post/4, get/2, get/3,
refresh_credentials/0, post/4,
request/5, request/6, request/7, refresh_credentials/0,
set_credentials/2, request/5, request/6, request/7,
has_credentials/0, set_credentials/2,
set_region/1, has_credentials/0,
ensure_imdsv2_token_valid/0, set_region/1,
api_get_request/2]). ensure_imdsv2_token_valid/0,
api_get_request/2
]).
%% gen-server exports %% gen-server exports
-export([start_link/0, -export([
init/1, start_link/0,
terminate/2, init/1,
code_change/3, terminate/2,
handle_call/3, code_change/3,
handle_cast/2, handle_call/3,
handle_info/2]). handle_cast/2,
handle_info/2
]).
%% Export all for unit tests %% Export all for unit tests
-ifdef(TEST). -ifdef(TEST).
@ -40,101 +44,110 @@
%% exported wrapper functions %% exported wrapper functions
%%==================================================================== %%====================================================================
-spec get(Service :: string(), -spec get(
Path :: path()) -> result(). Service :: string(),
Path :: path()
) -> result().
%% @doc Perform a HTTP GET request to the AWS API for the specified service. The %% @doc Perform a HTTP GET request to the AWS API for the specified service. The
%% response will automatically be decoded if it is either in JSON, or XML %% response will automatically be decoded if it is either in JSON, or XML
%% format. %% format.
%% @end %% @end
get(Service, Path) -> get(Service, Path) ->
get(Service, Path, []). get(Service, Path, []).
-spec get(
-spec get(Service :: string(), Service :: string(),
Path :: path(), Path :: path(),
Headers :: headers()) -> result(). Headers :: headers()
) -> result().
%% @doc Perform a HTTP GET request to the AWS API for the specified service. The %% @doc Perform a HTTP GET request to the AWS API for the specified service. The
%% response will automatically be decoded if it is either in JSON or XML %% response will automatically be decoded if it is either in JSON or XML
%% format. %% format.
%% @end %% @end
get(Service, Path, Headers) -> get(Service, Path, Headers) ->
request(Service, get, Path, "", Headers). request(Service, get, Path, "", Headers).
-spec post(
-spec post(Service :: string(), Service :: string(),
Path :: path(), Path :: path(),
Body :: body(), Body :: body(),
Headers :: headers()) -> result(). Headers :: headers()
) -> result().
%% @doc Perform a HTTP Post request to the AWS API for the specified service. The %% @doc Perform a HTTP Post request to the AWS API for the specified service. The
%% response will automatically be decoded if it is either in JSON or XML %% response will automatically be decoded if it is either in JSON or XML
%% format. %% format.
%% @end %% @end
post(Service, Path, Body, Headers) -> post(Service, Path, Body, Headers) ->
request(Service, post, Path, Body, Headers). request(Service, post, Path, Body, Headers).
-spec refresh_credentials() -> ok | error. -spec refresh_credentials() -> ok | error.
%% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service. %% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service.
%% @end %% @end
refresh_credentials() -> refresh_credentials() ->
gen_server:call(rabbitmq_aws, refresh_credentials). gen_server:call(rabbitmq_aws, refresh_credentials).
-spec refresh_credentials(state()) -> ok | error. -spec refresh_credentials(state()) -> ok | error.
%% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service. %% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service.
%% @end %% @end
refresh_credentials(State) -> refresh_credentials(State) ->
?LOG_DEBUG("Refreshing AWS credentials..."), ?LOG_DEBUG("Refreshing AWS credentials..."),
{_, NewState} = load_credentials(State), {_, NewState} = load_credentials(State),
?LOG_DEBUG("AWS credentials have been refreshed"), ?LOG_DEBUG("AWS credentials have been refreshed"),
set_credentials(NewState). set_credentials(NewState).
-spec request(
-spec request(Service :: string(), Service :: string(),
Method :: method(), Method :: method(),
Path :: path(), Path :: path(),
Body :: body(), Body :: body(),
Headers :: headers()) -> result(). Headers :: headers()
) -> result().
%% @doc Perform a HTTP request to the AWS API for the specified service. The %% @doc Perform a HTTP request to the AWS API for the specified service. The
%% response will automatically be decoded if it is either in JSON or XML %% response will automatically be decoded if it is either in JSON or XML
%% format. %% format.
%% @end %% @end
request(Service, Method, Path, Body, Headers) -> request(Service, Method, Path, Body, Headers) ->
gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, [], undefined}). gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, [], undefined}).
-spec request(
-spec request(Service :: string(), Service :: string(),
Method :: method(), Method :: method(),
Path :: path(), Path :: path(),
Body :: body(), Body :: body(),
Headers :: headers(), Headers :: headers(),
HTTPOptions :: http_options()) -> result(). HTTPOptions :: http_options()
) -> result().
%% @doc Perform a HTTP request to the AWS API for the specified service. The %% @doc Perform a HTTP request to the AWS API for the specified service. The
%% response will automatically be decoded if it is either in JSON or XML %% response will automatically be decoded if it is either in JSON or XML
%% format. %% format.
%% @end %% @end
request(Service, Method, Path, Body, Headers, HTTPOptions) -> request(Service, Method, Path, Body, Headers, HTTPOptions) ->
gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, undefined}). gen_server:call(
rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, undefined}
).
-spec request(
-spec request(Service :: string(), Service :: string(),
Method :: method(), Method :: method(),
Path :: path(), Path :: path(),
Body :: body(), Body :: body(),
Headers :: headers(), Headers :: headers(),
HTTPOptions :: http_options(), HTTPOptions :: http_options(),
Endpoint :: host()) -> result(). Endpoint :: host()
) -> result().
%% @doc Perform a HTTP request to the AWS API for the specified service, overriding %% @doc Perform a HTTP request to the AWS API for the specified service, overriding
%% the endpoint URL to use when invoking the API. This is useful for local testing %% the endpoint URL to use when invoking the API. This is useful for local testing
%% of services such as DynamoDB. The response will automatically be decoded %% of services such as DynamoDB. The response will automatically be decoded
%% if it is either in JSON or XML format. %% if it is either in JSON or XML format.
%% @end %% @end
request(Service, Method, Path, Body, Headers, HTTPOptions, Endpoint) -> request(Service, Method, Path, Body, Headers, HTTPOptions, Endpoint) ->
gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, Endpoint}). gen_server:call(
rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, Endpoint}
).
-spec set_credentials(state()) -> ok. -spec set_credentials(state()) -> ok.
set_credentials(NewState) -> set_credentials(NewState) ->
gen_server:call(rabbitmq_aws, {set_credentials, NewState}). gen_server:call(rabbitmq_aws, {set_credentials, NewState}).
-spec set_credentials(access_key(), secret_access_key()) -> ok. -spec set_credentials(access_key(), secret_access_key()) -> ok.
%% @doc Manually set the access credentials for requests. This should %% @doc Manually set the access credentials for requests. This should
@ -143,122 +156,113 @@ set_credentials(NewState) ->
%% configuration or the AWS Instance Metadata service. %% configuration or the AWS Instance Metadata service.
%% @end %% @end
set_credentials(AccessKey, SecretAccessKey) -> set_credentials(AccessKey, SecretAccessKey) ->
gen_server:call(rabbitmq_aws, {set_credentials, AccessKey, SecretAccessKey}). gen_server:call(rabbitmq_aws, {set_credentials, AccessKey, SecretAccessKey}).
-spec set_region(Region :: string()) -> ok. -spec set_region(Region :: string()) -> ok.
%% @doc Manually set the AWS region to perform API requests to. %% @doc Manually set the AWS region to perform API requests to.
%% @end %% @end
set_region(Region) -> set_region(Region) ->
gen_server:call(rabbitmq_aws, {set_region, Region}). gen_server:call(rabbitmq_aws, {set_region, Region}).
-spec set_imdsv2_token(imdsv2token()) -> ok. -spec set_imdsv2_token(imdsv2token()) -> ok.
%% @doc Manually set the Imdsv2Token used to perform instance metadata service requests. %% @doc Manually set the Imdsv2Token used to perform instance metadata service requests.
%% @end %% @end
set_imdsv2_token(Imdsv2Token) -> set_imdsv2_token(Imdsv2Token) ->
gen_server:call(rabbitmq_aws, {set_imdsv2_token, Imdsv2Token}). gen_server:call(rabbitmq_aws, {set_imdsv2_token, Imdsv2Token}).
-spec get_imdsv2_token() -> imdsv2token() | 'undefined'. -spec get_imdsv2_token() -> imdsv2token() | 'undefined'.
%% @doc return the current Imdsv2Token used to perform instance metadata service requests. %% @doc return the current Imdsv2Token used to perform instance metadata service requests.
%% @end %% @end
get_imdsv2_token() -> get_imdsv2_token() ->
{ok, Imdsv2Token} = gen_server:call(rabbitmq_aws, get_imdsv2_token), {ok, Imdsv2Token} = gen_server:call(rabbitmq_aws, get_imdsv2_token),
Imdsv2Token. Imdsv2Token.
%%==================================================================== %%====================================================================
%% gen_server functions %% gen_server functions
%%==================================================================== %%====================================================================
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec init(list()) -> {ok, state()}. -spec init(list()) -> {ok, state()}.
init([]) -> init([]) ->
{ok, #state{}}. {ok, #state{}}.
terminate(_, _) -> terminate(_, _) ->
ok. ok.
code_change(_, _, State) -> code_change(_, _, State) ->
{ok, State}. {ok, State}.
handle_call(Msg, _From, State) -> handle_call(Msg, _From, State) ->
handle_msg(Msg, State). handle_msg(Msg, State).
handle_cast(_Request, State) -> handle_cast(_Request, State) ->
{noreply, State}. {noreply, State}.
handle_info(_Info, State) -> handle_info(_Info, State) ->
{noreply, State}. {noreply, State}.
%%==================================================================== %%====================================================================
%% Internal functions %% Internal functions
%%==================================================================== %%====================================================================
handle_msg({request, Service, Method, Headers, Path, Body, Options, Host}, State) -> handle_msg({request, Service, Method, Headers, Path, Body, Options, Host}, State) ->
{Response, NewState} = perform_request(State, Service, Method, Headers, Path, Body, Options, Host), {Response, NewState} = perform_request(
State, Service, Method, Headers, Path, Body, Options, Host
),
{reply, Response, NewState}; {reply, Response, NewState};
handle_msg(get_state, State) -> handle_msg(get_state, State) ->
{reply, {ok, State}, State}; {reply, {ok, State}, State};
handle_msg(refresh_credentials, State) -> handle_msg(refresh_credentials, State) ->
{Reply, NewState} = load_credentials(State), {Reply, NewState} = load_credentials(State),
{reply, Reply, NewState}; {reply, Reply, NewState};
handle_msg({set_credentials, AccessKey, SecretAccessKey}, State) -> handle_msg({set_credentials, AccessKey, SecretAccessKey}, State) ->
{reply, ok, State#state{access_key = AccessKey, {reply, ok, State#state{
secret_access_key = SecretAccessKey, access_key = AccessKey,
security_token = undefined, secret_access_key = SecretAccessKey,
expiration = undefined, security_token = undefined,
error = undefined}}; expiration = undefined,
error = undefined
}};
handle_msg({set_credentials, NewState}, State) -> handle_msg({set_credentials, NewState}, State) ->
{reply, ok, State#state{access_key = NewState#state.access_key, {reply, ok, State#state{
secret_access_key = NewState#state.secret_access_key, access_key = NewState#state.access_key,
security_token = NewState#state.security_token, secret_access_key = NewState#state.secret_access_key,
expiration = NewState#state.expiration, security_token = NewState#state.security_token,
error = NewState#state.error}}; expiration = NewState#state.expiration,
error = NewState#state.error
}};
handle_msg({set_region, Region}, State) -> handle_msg({set_region, Region}, State) ->
{reply, ok, State#state{region = Region}}; {reply, ok, State#state{region = Region}};
handle_msg({set_imdsv2_token, Imdsv2Token}, State) -> handle_msg({set_imdsv2_token, Imdsv2Token}, State) ->
{reply, ok, State#state{imdsv2_token = Imdsv2Token}}; {reply, ok, State#state{imdsv2_token = Imdsv2Token}};
handle_msg(has_credentials, State) -> handle_msg(has_credentials, State) ->
{reply, has_credentials(State), State}; {reply, has_credentials(State), State};
handle_msg(get_imdsv2_token, State) -> handle_msg(get_imdsv2_token, State) ->
{reply, {ok, State#state.imdsv2_token}, State}; {reply, {ok, State#state.imdsv2_token}, State};
handle_msg(_Request, State) -> handle_msg(_Request, State) ->
{noreply, State}. {noreply, State}.
-spec endpoint(
-spec endpoint(State :: state(), Host :: string(), State :: state(),
Service :: string(), Path :: string()) -> string(). Host :: string(),
Service :: string(),
Path :: string()
) -> string().
%% @doc Return the endpoint URL, either by constructing it with the service %% @doc Return the endpoint URL, either by constructing it with the service
%% information passed in, or by using the passed in Host value. %% information passed in, or by using the passed in Host value.
%% @ednd %% @ednd
endpoint(#state{region = Region}, undefined, Service, Path) -> endpoint(#state{region = Region}, undefined, Service, Path) ->
lists:flatten(["https://", endpoint_host(Region, Service), Path]); lists:flatten(["https://", endpoint_host(Region, Service), Path]);
endpoint(_, Host, _, Path) -> endpoint(_, Host, _, Path) ->
lists:flatten(["https://", Host, Path]). lists:flatten(["https://", Host, Path]).
-spec endpoint_host(Region :: region(), Service :: string()) -> host(). -spec endpoint_host(Region :: region(), Service :: string()) -> host().
%% @doc Construct the endpoint hostname for the request based upon the service %% @doc Construct the endpoint hostname for the request based upon the service
%% and region. %% and region.
%% @end %% @end
endpoint_host(Region, Service) -> endpoint_host(Region, Service) ->
lists:flatten(string:join([Service, Region, endpoint_tld(Region)], ".")). lists:flatten(string:join([Service, Region, endpoint_tld(Region)], ".")).
-spec endpoint_tld(Region :: region()) -> host(). -spec endpoint_tld(Region :: region()) -> host().
%% @doc Construct the endpoint hostname TLD for the request based upon the region. %% @doc Construct the endpoint hostname TLD for the request based upon the region.
@ -277,27 +281,29 @@ endpoint_tld(_Other) ->
%% maybe_decode_body/2 method. %% maybe_decode_body/2 method.
%% @end %% @end
format_response({ok, {{_Version, 200, _Message}, Headers, Body}}) -> format_response({ok, {{_Version, 200, _Message}, Headers, Body}}) ->
{ok, {Headers, maybe_decode_body(get_content_type(Headers), Body)}}; {ok, {Headers, maybe_decode_body(get_content_type(Headers), Body)}};
format_response({ok, {{_Version, StatusCode, Message}, Headers, Body}}) when StatusCode >= 400 -> format_response({ok, {{_Version, StatusCode, Message}, Headers, Body}}) when StatusCode >= 400 ->
{error, Message, {Headers, maybe_decode_body(get_content_type(Headers), Body)}}; {error, Message, {Headers, maybe_decode_body(get_content_type(Headers), Body)}};
format_response({error, Reason}) -> format_response({error, Reason}) ->
{error, Reason, undefined}. {error, Reason, undefined}.
-spec get_content_type(Headers :: headers()) -> {Type :: string(), Subtype :: string()}. -spec get_content_type(Headers :: headers()) -> {Type :: string(), Subtype :: string()}.
%% @doc Fetch the content type from the headers and return it as a tuple of %% @doc Fetch the content type from the headers and return it as a tuple of
%% {Type, Subtype}. %% {Type, Subtype}.
%% @end %% @end
get_content_type(Headers) -> get_content_type(Headers) ->
Value = case proplists:get_value("content-type", Headers, undefined) of Value =
undefined -> case proplists:get_value("content-type", Headers, undefined) of
proplists:get_value("Content-Type", Headers, "text/xml"); undefined ->
Other -> Other proplists:get_value("Content-Type", Headers, "text/xml");
end, Other ->
parse_content_type(Value). Other
end,
parse_content_type(Value).
-spec has_credentials() -> boolean(). -spec has_credentials() -> boolean().
has_credentials() -> has_credentials() ->
gen_server:call(rabbitmq_aws, has_credentials). gen_server:call(rabbitmq_aws, has_credentials).
-spec has_credentials(state()) -> boolean(). -spec has_credentials(state()) -> boolean().
%% @doc check to see if there are credentials made available in the current state %% @doc check to see if there are credentials made available in the current state
@ -307,16 +313,15 @@ has_credentials(#state{error = Error}) when Error /= undefined -> false;
has_credentials(#state{access_key = Key}) when Key /= undefined -> true; has_credentials(#state{access_key = Key}) when Key /= undefined -> true;
has_credentials(_) -> false. has_credentials(_) -> false.
-spec expired_credentials(Expiration :: calendar:datetime()) -> boolean(). -spec expired_credentials(Expiration :: calendar:datetime()) -> boolean().
%% @doc Indicates if the date that is passed in has expired. %% @doc Indicates if the date that is passed in has expired.
%% end %% end
expired_credentials(undefined) -> false; expired_credentials(undefined) ->
false;
expired_credentials(Expiration) -> expired_credentials(Expiration) ->
Now = calendar:datetime_to_gregorian_seconds(local_time()), Now = calendar:datetime_to_gregorian_seconds(local_time()),
Expires = calendar:datetime_to_gregorian_seconds(Expiration), Expires = calendar:datetime_to_gregorian_seconds(Expiration),
Now >= Expires. Now >= Expires.
-spec load_credentials(State :: state()) -> {ok, state()} | {error, state()}. -spec load_credentials(State :: state()) -> {ok, state()} | {error, state()}.
%% @doc Load the credentials using the following order of configuration precedence: %% @doc Load the credentials using the following order of configuration precedence:
@ -325,138 +330,188 @@ expired_credentials(Expiration) ->
%% - EC2 Instance Metadata Service %% - EC2 Instance Metadata Service
%% @end %% @end
load_credentials(#state{region = Region}) -> load_credentials(#state{region = Region}) ->
case rabbitmq_aws_config:credentials() of case rabbitmq_aws_config:credentials() of
{ok, AccessKey, SecretAccessKey, Expiration, SecurityToken} -> {ok, AccessKey, SecretAccessKey, Expiration, SecurityToken} ->
{ok, #state{region = Region, {ok, #state{
error = undefined, region = Region,
access_key = AccessKey, error = undefined,
secret_access_key = SecretAccessKey, access_key = AccessKey,
expiration = Expiration, secret_access_key = SecretAccessKey,
security_token = SecurityToken, expiration = Expiration,
imdsv2_token = undefined}}; security_token = SecurityToken,
{error, Reason} -> imdsv2_token = undefined
?LOG_ERROR("Could not load AWS credentials from environment variables, AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE or EC2 metadata endpoint: ~tp. Will depend on config settings to be set~n", [Reason]), }};
{error, #state{region = Region, {error, Reason} ->
error = Reason, ?LOG_ERROR(
access_key = undefined, "Could not load AWS credentials from environment variables, AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE or EC2 metadata endpoint: ~tp. Will depend on config settings to be set~n",
secret_access_key = undefined, [Reason]
expiration = undefined, ),
security_token = undefined, {error, #state{
imdsv2_token = undefined}} region = Region,
end. error = Reason,
access_key = undefined,
secret_access_key = undefined,
expiration = undefined,
security_token = undefined,
imdsv2_token = undefined
}}
end.
-spec local_time() -> calendar:datetime(). -spec local_time() -> calendar:datetime().
%% @doc Return the current local time. %% @doc Return the current local time.
%% @end %% @end
local_time() -> local_time() ->
[Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()),
Value. Value.
-spec maybe_decode_body(ContentType :: {nonempty_string(), nonempty_string()}, Body :: body()) ->
-spec maybe_decode_body(ContentType :: {nonempty_string(), nonempty_string()}, Body :: body()) -> list() | body(). list() | body().
%% @doc Attempt to decode the response body by its MIME %% @doc Attempt to decode the response body by its MIME
%% @end %% @end
maybe_decode_body({"application", "x-amz-json-1.0"}, Body) -> maybe_decode_body({"application", "x-amz-json-1.0"}, Body) ->
rabbitmq_aws_json:decode(Body); rabbitmq_aws_json:decode(Body);
maybe_decode_body({"application", "json"}, Body) -> maybe_decode_body({"application", "json"}, Body) ->
rabbitmq_aws_json:decode(Body); rabbitmq_aws_json:decode(Body);
maybe_decode_body({_, "xml"}, Body) -> maybe_decode_body({_, "xml"}, Body) ->
rabbitmq_aws_xml:parse(Body); rabbitmq_aws_xml:parse(Body);
maybe_decode_body(_ContentType, Body) -> maybe_decode_body(_ContentType, Body) ->
Body. Body.
-spec parse_content_type(ContentType :: string()) -> {Type :: string(), Subtype :: string()}. -spec parse_content_type(ContentType :: string()) -> {Type :: string(), Subtype :: string()}.
%% @doc parse a content type string returning a tuple of type/subtype %% @doc parse a content type string returning a tuple of type/subtype
%% @end %% @end
parse_content_type(ContentType) -> parse_content_type(ContentType) ->
Parts = string:tokens(ContentType, ";"), Parts = string:tokens(ContentType, ";"),
[Type, Subtype] = string:tokens(lists:nth(1, Parts), "/"), [Type, Subtype] = string:tokens(lists:nth(1, Parts), "/"),
{Type, Subtype}. {Type, Subtype}.
-spec perform_request(
-spec perform_request(State :: state(), Service :: string(), Method :: method(), State :: state(),
Headers :: headers(), Path :: path(), Body :: body(), Service :: string(),
Options :: http_options(), Host :: string() | undefined) Method :: method(),
-> {Result :: result(), NewState :: state()}. Headers :: headers(),
Path :: path(),
Body :: body(),
Options :: http_options(),
Host :: string() | undefined
) ->
{Result :: result(), NewState :: state()}.
%% @doc Make the API request and return the formatted response. %% @doc Make the API request and return the formatted response.
%% @end %% @end
perform_request(State, Service, Method, Headers, Path, Body, Options, Host) -> perform_request(State, Service, Method, Headers, Path, Body, Options, Host) ->
perform_request_has_creds(has_credentials(State), State, Service, Method, perform_request_has_creds(
Headers, Path, Body, Options, Host). has_credentials(State),
State,
Service,
Method,
Headers,
Path,
Body,
Options,
Host
).
-spec perform_request_has_creds(
-spec perform_request_has_creds(HasCreds :: boolean(), State :: state(), HasCreds :: boolean(),
Service :: string(), Method :: method(), State :: state(),
Headers :: headers(), Path :: path(), Body :: body(), Service :: string(),
Options :: http_options(), Host :: string() | undefined) Method :: method(),
-> {Result :: result(), NewState :: state()}. Headers :: headers(),
Path :: path(),
Body :: body(),
Options :: http_options(),
Host :: string() | undefined
) ->
{Result :: result(), NewState :: state()}.
%% @doc Invoked after checking to see if there are credentials. If there are, %% @doc Invoked after checking to see if there are credentials. If there are,
%% validate they have not or will not expire, performing the request if not, %% validate they have not or will not expire, performing the request if not,
%% otherwise return an error result. %% otherwise return an error result.
%% @end %% @end
perform_request_has_creds(true, State, Service, Method, Headers, Path, Body, Options, Host) -> perform_request_has_creds(true, State, Service, Method, Headers, Path, Body, Options, Host) ->
perform_request_creds_expired(expired_credentials(State#state.expiration), State, perform_request_creds_expired(
Service, Method, Headers, Path, Body, Options, Host); expired_credentials(State#state.expiration),
State,
Service,
Method,
Headers,
Path,
Body,
Options,
Host
);
perform_request_has_creds(false, State, _, _, _, _, _, _, _) -> perform_request_has_creds(false, State, _, _, _, _, _, _, _) ->
perform_request_creds_error(State). perform_request_creds_error(State).
-spec perform_request_creds_expired(
-spec perform_request_creds_expired(CredsExp :: boolean(), State :: state(), CredsExp :: boolean(),
Service :: string(), Method :: method(), State :: state(),
Headers :: headers(), Path :: path(), Body :: body(), Service :: string(),
Options :: http_options(), Host :: string() | undefined) Method :: method(),
-> {Result :: result(), NewState :: state()}. Headers :: headers(),
Path :: path(),
Body :: body(),
Options :: http_options(),
Host :: string() | undefined
) ->
{Result :: result(), NewState :: state()}.
%% @doc Invoked after checking to see if the current credentials have expired. %% @doc Invoked after checking to see if the current credentials have expired.
%% If they haven't, perform the request, otherwise try and refresh the %% If they haven't, perform the request, otherwise try and refresh the
%% credentials before performing the request. %% credentials before performing the request.
%% @end %% @end
perform_request_creds_expired(false, State, Service, Method, Headers, Path, Body, Options, Host) -> perform_request_creds_expired(false, State, Service, Method, Headers, Path, Body, Options, Host) ->
perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options, Host); perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options, Host);
perform_request_creds_expired(true, State, _, _, _, _, _, _, _) -> perform_request_creds_expired(true, State, _, _, _, _, _, _, _) ->
perform_request_creds_error(State#state{error = "Credentials expired!"}). perform_request_creds_error(State#state{error = "Credentials expired!"}).
-spec perform_request_with_creds(
-spec perform_request_with_creds(State :: state(), Service :: string(), Method :: method(), State :: state(),
Headers :: headers(), Path :: path(), Body :: body(), Service :: string(),
Options :: http_options(), Host :: string() | undefined) Method :: method(),
-> {Result :: result(), NewState :: state()}. Headers :: headers(),
Path :: path(),
Body :: body(),
Options :: http_options(),
Host :: string() | undefined
) ->
{Result :: result(), NewState :: state()}.
%% @doc Once it is validated that there are credentials to try and that they have not %% @doc Once it is validated that there are credentials to try and that they have not
%% expired, perform the request and return the response. %% expired, perform the request and return the response.
%% @end %% @end
perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options, Host) -> perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options, Host) ->
URI = endpoint(State, Host, Service, Path), URI = endpoint(State, Host, Service, Path),
SignedHeaders = sign_headers(State, Service, Method, URI, Headers, Body), SignedHeaders = sign_headers(State, Service, Method, URI, Headers, Body),
ContentType = proplists:get_value("content-type", SignedHeaders, undefined), ContentType = proplists:get_value("content-type", SignedHeaders, undefined),
perform_request_with_creds(State, Method, URI, SignedHeaders, ContentType, Body, Options). perform_request_with_creds(State, Method, URI, SignedHeaders, ContentType, Body, Options).
-spec perform_request_with_creds(
-spec perform_request_with_creds(State :: state(), Method :: method(), URI :: string(), State :: state(),
Headers :: headers(), ContentType :: string() | undefined, Method :: method(),
Body :: body(), Options :: http_options()) URI :: string(),
-> {Result :: result(), NewState :: state()}. Headers :: headers(),
ContentType :: string() | undefined,
Body :: body(),
Options :: http_options()
) ->
{Result :: result(), NewState :: state()}.
%% @doc Once it is validated that there are credentials to try and that they have not %% @doc Once it is validated that there are credentials to try and that they have not
%% expired, perform the request and return the response. %% expired, perform the request and return the response.
%% @end %% @end
perform_request_with_creds(State, Method, URI, Headers, undefined, "", Options0) -> perform_request_with_creds(State, Method, URI, Headers, undefined, "", Options0) ->
Options1 = ensure_timeout(Options0), Options1 = ensure_timeout(Options0),
Response = httpc:request(Method, {URI, Headers}, Options1, []), Response = httpc:request(Method, {URI, Headers}, Options1, []),
{format_response(Response), State}; {format_response(Response), State};
perform_request_with_creds(State, Method, URI, Headers, ContentType, Body, Options0) -> perform_request_with_creds(State, Method, URI, Headers, ContentType, Body, Options0) ->
Options1 = ensure_timeout(Options0), Options1 = ensure_timeout(Options0),
Response = httpc:request(Method, {URI, Headers, ContentType, Body}, Options1, []), Response = httpc:request(Method, {URI, Headers, ContentType, Body}, Options1, []),
{format_response(Response), State}. {format_response(Response), State}.
-spec perform_request_creds_error(State :: state()) -> -spec perform_request_creds_error(State :: state()) ->
{result_error(), NewState :: state()}. {result_error(), NewState :: state()}.
%% @doc Return the error response when there are not any credentials to use with %% @doc Return the error response when there are not any credentials to use with
%% the request. %% the request.
%% @end %% @end
perform_request_creds_error(State) -> perform_request_creds_error(State) ->
{{error, {credentials, State#state.error}}, State}. {{error, {credentials, State#state.error}}, State}.
%% @doc Ensure that the timeout option is set and greater than 0 and less %% @doc Ensure that the timeout option is set and greater than 0 and less
%% than about 1/2 of the default gen_server:call timeout. This gives %% than about 1/2 of the default gen_server:call timeout. This gives
@ -474,52 +529,72 @@ ensure_timeout(Options) ->
Options1 ++ [{timeout, ?DEFAULT_HTTP_TIMEOUT}] Options1 ++ [{timeout, ?DEFAULT_HTTP_TIMEOUT}]
end. end.
-spec sign_headers(
-spec sign_headers(State :: state(), Service :: string(), Method :: method(), State :: state(),
URI :: string(), Headers :: headers(), Body :: body()) -> headers(). Service :: string(),
Method :: method(),
URI :: string(),
Headers :: headers(),
Body :: body()
) -> headers().
%% @doc Build the signed headers for the API request. %% @doc Build the signed headers for the API request.
%% @end %% @end
sign_headers(#state{access_key = AccessKey, sign_headers(
secret_access_key = SecretKey, #state{
security_token = SecurityToken, access_key = AccessKey,
region = Region}, Service, Method, URI, Headers, Body) -> secret_access_key = SecretKey,
rabbitmq_aws_sign:headers(#request{access_key = AccessKey, security_token = SecurityToken,
secret_access_key = SecretKey, region = Region
security_token = SecurityToken, },
region = Region, Service,
service = Service, Method,
method = Method, URI,
uri = URI, Headers,
headers = Headers, Body
body = Body}). ) ->
rabbitmq_aws_sign:headers(#request{
access_key = AccessKey,
secret_access_key = SecretKey,
security_token = SecurityToken,
region = Region,
service = Service,
method = Method,
uri = URI,
headers = Headers,
body = Body
}).
-spec expired_imdsv2_token('undefined' | imdsv2token()) -> boolean(). -spec expired_imdsv2_token('undefined' | imdsv2token()) -> boolean().
%% @doc Determine whether or not an Imdsv2Token has expired. %% @doc Determine whether or not an Imdsv2Token has expired.
%% @end %% @end
expired_imdsv2_token(undefined) -> expired_imdsv2_token(undefined) ->
?LOG_DEBUG("EC2 IMDSv2 token has not yet been obtained"), ?LOG_DEBUG("EC2 IMDSv2 token has not yet been obtained"),
true; true;
expired_imdsv2_token({_, _, undefined}) -> expired_imdsv2_token({_, _, undefined}) ->
?LOG_DEBUG("EC2 IMDSv2 token is not available"), ?LOG_DEBUG("EC2 IMDSv2 token is not available"),
true; true;
expired_imdsv2_token({_, _, Expiration}) -> expired_imdsv2_token({_, _, Expiration}) ->
Now = calendar:datetime_to_gregorian_seconds(local_time()), Now = calendar:datetime_to_gregorian_seconds(local_time()),
HasExpired = Now >= Expiration, HasExpired = Now >= Expiration,
?LOG_DEBUG("EC2 IMDSv2 token has expired: ~tp", [HasExpired]), ?LOG_DEBUG("EC2 IMDSv2 token has expired: ~tp", [HasExpired]),
HasExpired. HasExpired.
-spec ensure_imdsv2_token_valid() -> security_token(). -spec ensure_imdsv2_token_valid() -> security_token().
ensure_imdsv2_token_valid() -> ensure_imdsv2_token_valid() ->
Imdsv2Token = get_imdsv2_token(), Imdsv2Token = get_imdsv2_token(),
case expired_imdsv2_token(Imdsv2Token) of case expired_imdsv2_token(Imdsv2Token) of
true -> Value = rabbitmq_aws_config:load_imdsv2_token(), true ->
Expiration = calendar:datetime_to_gregorian_seconds(local_time()) + ?METADATA_TOKEN_TTL_SECONDS, Value = rabbitmq_aws_config:load_imdsv2_token(),
set_imdsv2_token(#imdsv2token{token = Value, Expiration =
expiration = Expiration}), calendar:datetime_to_gregorian_seconds(local_time()) + ?METADATA_TOKEN_TTL_SECONDS,
set_imdsv2_token(#imdsv2token{
token = Value,
expiration = Expiration
}),
Value; Value;
_ -> Imdsv2Token#imdsv2token.token _ ->
end. Imdsv2Token#imdsv2token.token
end.
-spec ensure_credentials_valid() -> ok. -spec ensure_credentials_valid() -> ok.
%% @doc Invoked before each AWS service API request to check if the current credentials are available and that they have not expired. %% @doc Invoked before each AWS service API request to check if the current credentials are available and that they have not expired.
@ -527,43 +602,49 @@ ensure_imdsv2_token_valid() ->
%% If the credentials are not available or have expired, then refresh them before performing the request. %% If the credentials are not available or have expired, then refresh them before performing the request.
%% @end %% @end
ensure_credentials_valid() -> ensure_credentials_valid() ->
?LOG_DEBUG("Making sure AWS credentials are available and still valid"), ?LOG_DEBUG("Making sure AWS credentials are available and still valid"),
{ok, State} = gen_server:call(rabbitmq_aws, get_state), {ok, State} = gen_server:call(rabbitmq_aws, get_state),
case has_credentials(State) of case has_credentials(State) of
true -> case expired_credentials(State#state.expiration) of true ->
true -> refresh_credentials(State); case expired_credentials(State#state.expiration) of
_ -> ok true -> refresh_credentials(State);
_ -> ok
end; end;
_ -> refresh_credentials(State) _ ->
end. refresh_credentials(State)
end.
-spec api_get_request(string(), path()) -> {'ok', list()} | {'error', term()}. -spec api_get_request(string(), path()) -> {'ok', list()} | {'error', term()}.
%% @doc Invoke an API call to an AWS service. %% @doc Invoke an API call to an AWS service.
%% @end %% @end
api_get_request(Service, Path) -> api_get_request(Service, Path) ->
?LOG_DEBUG("Invoking AWS request {Service: ~tp; Path: ~tp}...", [Service, Path]), ?LOG_DEBUG("Invoking AWS request {Service: ~tp; Path: ~tp}...", [Service, Path]),
api_get_request_with_retries(Service, Path, ?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS). api_get_request_with_retries(Service, Path, ?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS).
-spec api_get_request_with_retries(string(), path(), integer(), integer()) ->
-spec api_get_request_with_retries(string(), path(), integer(), integer()) -> {'ok', list()} | {'error', term()}. {'ok', list()} | {'error', term()}.
%% @doc Invoke an API call to an AWS service with retries. %% @doc Invoke an API call to an AWS service with retries.
%% @end %% @end
api_get_request_with_retries(_, _, 0, _) -> api_get_request_with_retries(_, _, 0, _) ->
?LOG_WARNING("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]), ?LOG_WARNING("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]),
{error, "AWS service is unavailable"}; {error, "AWS service is unavailable"};
api_get_request_with_retries(Service, Path, Retries, WaitTimeBetweenRetries) -> api_get_request_with_retries(Service, Path, Retries, WaitTimeBetweenRetries) ->
ensure_credentials_valid(), ensure_credentials_valid(),
case get(Service, Path) of case get(Service, Path) of
{ok, {_Headers, Payload}} -> ?LOG_DEBUG("AWS request: ~ts~nResponse: ~tp", [Path, Payload]), {ok, {_Headers, Payload}} ->
{ok, Payload}; ?LOG_DEBUG("AWS request: ~ts~nResponse: ~tp", [Path, Payload]),
{error, {credentials, _}} -> {error, credentials}; {ok, Payload};
{error, Message, Response} -> ?LOG_WARNING("Error occurred: ~ts", [Message]), {error, {credentials, _}} ->
case Response of {error, credentials};
{_, Payload} -> ?LOG_WARNING("Failed AWS request: ~ts~nResponse: ~tp", [Path, Payload]); {error, Message, Response} ->
_ -> ok ?LOG_WARNING("Error occurred: ~ts", [Message]),
end, case Response of
?LOG_WARNING("Will retry AWS request, remaining retries: ~b", [Retries]), {_, Payload} ->
timer:sleep(WaitTimeBetweenRetries), ?LOG_WARNING("Failed AWS request: ~ts~nResponse: ~tp", [Path, Payload]);
api_get_request_with_retries(Service, Path, Retries - 1, WaitTimeBetweenRetries) _ ->
end. ok
end,
?LOG_WARNING("Will retry AWS request, remaining retries: ~b", [Retries]),
timer:sleep(WaitTimeBetweenRetries),
api_get_request_with_retries(Service, Path, Retries - 1, WaitTimeBetweenRetries)
end.

View File

@ -16,7 +16,7 @@
%% =================================================================== %% ===================================================================
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
rabbitmq_aws_sup:start_link(). rabbitmq_aws_sup:start_link().
stop(_State) -> stop(_State) ->
ok. ok.

View File

@ -9,20 +9,22 @@
-module(rabbitmq_aws_config). -module(rabbitmq_aws_config).
%% API %% API
-export([credentials/0, -export([
credentials/1, credentials/0,
value/2, credentials/1,
values/1, value/2,
instance_metadata_url/1, values/1,
instance_credentials_url/1, instance_metadata_url/1,
instance_availability_zone_url/0, instance_credentials_url/1,
instance_role_url/0, instance_availability_zone_url/0,
instance_id_url/0, instance_role_url/0,
instance_id/0, instance_id_url/0,
load_imdsv2_token/0, instance_id/0,
instance_metadata_request_headers/0, load_imdsv2_token/0,
region/0, instance_metadata_request_headers/0,
region/1]). region/0,
region/1
]).
%% Export all for unit tests %% Export all for unit tests
-ifdef(TEST). -ifdef(TEST).
@ -81,7 +83,7 @@
%% will be returned. %% will be returned.
%% @end %% @end
credentials() -> credentials() ->
credentials(profile()). credentials(profile()).
-spec credentials(string()) -> security_credentials(). -spec credentials(string()) -> security_credentials().
%% @doc Return the credentials from environment variables, configuration or the %% @doc Return the credentials from environment variables, configuration or the
@ -129,10 +131,11 @@ credentials() ->
%% will be returned. %% will be returned.
%% @end %% @end
credentials(Profile) -> credentials(Profile) ->
lookup_credentials(Profile, lookup_credentials(
os:getenv("AWS_ACCESS_KEY_ID"), Profile,
os:getenv("AWS_SECRET_ACCESS_KEY")). os:getenv("AWS_ACCESS_KEY_ID"),
os:getenv("AWS_SECRET_ACCESS_KEY")
).
-spec region() -> {ok, string()}. -spec region() -> {ok, string()}.
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment %% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
@ -144,8 +147,7 @@ credentials(Profile) ->
%% local instance metadata server. %% local instance metadata server.
%% @end %% @end
region() -> region() ->
region(profile()). region(profile()).
-spec region(Region :: string()) -> {ok, region()}. -spec region(Region :: string()) -> {ok, region()}.
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment %% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
@ -157,60 +159,61 @@ region() ->
%% local instance metadata server. %% local instance metadata server.
%% @end %% @end
region(Profile) -> region(Profile) ->
case lookup_region(Profile, os:getenv("AWS_DEFAULT_REGION")) of case lookup_region(Profile, os:getenv("AWS_DEFAULT_REGION")) of
{ok, Region} -> {ok, Region}; {ok, Region} -> {ok, Region};
_ -> {ok, ?DEFAULT_REGION} _ -> {ok, ?DEFAULT_REGION}
end. end.
-spec instance_id() -> {'ok', string()} | {'error', 'undefined'}. -spec instance_id() -> {'ok', string()} | {'error', 'undefined'}.
%% @doc Return the instance ID from the EC2 metadata service. %% @doc Return the instance ID from the EC2 metadata service.
%% @end %% @end
instance_id() -> instance_id() ->
URL = instance_id_url(), URL = instance_id_url(),
parse_body_response(perform_http_get_instance_metadata(URL)). parse_body_response(perform_http_get_instance_metadata(URL)).
-spec value(Profile :: string(), Key :: atom()) ->
-spec value(Profile :: string(), Key :: atom()) Value :: any() | {error, Reason :: atom()}.
-> Value :: any() | {error, Reason :: atom()}.
%% @doc Return the configuration data for the specified profile or an error %% @doc Return the configuration data for the specified profile or an error
%% if the profile is not found. %% if the profile is not found.
%% @end %% @end
value(Profile, Key) -> value(Profile, Key) ->
get_value(Key, values(Profile)). get_value(Key, values(Profile)).
-spec values(Profile :: string()) ->
-spec values(Profile :: string()) Settings ::
-> Settings :: list() list()
| {error, Reason :: atom()}. | {error, Reason :: atom()}.
%% @doc Return the configuration data for the specified profile or an error %% @doc Return the configuration data for the specified profile or an error
%% if the profile is not found. %% if the profile is not found.
%% @end %% @end
values(Profile) -> values(Profile) ->
case config_file_data() of case config_file_data() of
{error, Reason} -> {error, Reason} ->
{error, Reason}; {error, Reason};
Settings -> Settings ->
Prefixed = lists:flatten(["profile ", Profile]), Prefixed = lists:flatten(["profile ", Profile]),
proplists:get_value(Profile, Settings, proplists:get_value(
proplists:get_value(Prefixed, Profile,
Settings, {error, undefined})) Settings,
end. proplists:get_value(
Prefixed,
Settings,
{error, undefined}
)
)
end.
%% ----------------------------------------------------------------------------- %% -----------------------------------------------------------------------------
%% Private / Internal Methods %% Private / Internal Methods
%% ----------------------------------------------------------------------------- %% -----------------------------------------------------------------------------
-spec config_file() -> string(). -spec config_file() -> string().
%% @doc Return the configuration file to test using either the value of the %% @doc Return the configuration file to test using either the value of the
%% AWS_CONFIG_FILE or the default location where the file is expected to %% AWS_CONFIG_FILE or the default location where the file is expected to
%% exist. %% exist.
%% @end %% @end
config_file() -> config_file() ->
config_file(os:getenv("AWS_CONFIG_FILE")). config_file(os:getenv("AWS_CONFIG_FILE")).
-spec config_file(Path :: false | string()) -> string(). -spec config_file(Path :: false | string()) -> string().
%% @doc Return the configuration file to test using either the value of the %% @doc Return the configuration file to test using either the value of the
@ -218,17 +221,15 @@ config_file() ->
%% exist. %% exist.
%% @end %% @end
config_file(false) -> config_file(false) ->
filename:join([home_path(), ".aws", "config"]); filename:join([home_path(), ".aws", "config"]);
config_file(EnvVar) -> config_file(EnvVar) ->
EnvVar. EnvVar.
-spec config_file_data() -> list() | {error, Reason :: atom()}. -spec config_file_data() -> list() | {error, Reason :: atom()}.
%% @doc Return the values from a configuration file as a proplist by section %% @doc Return the values from a configuration file as a proplist by section
%% @end %% @end
config_file_data() -> config_file_data() ->
ini_file_data(config_file()). ini_file_data(config_file()).
-spec credentials_file() -> string(). -spec credentials_file() -> string().
%% @doc Return the shared credentials file to test using either the value of the %% @doc Return the shared credentials file to test using either the value of the
@ -236,8 +237,7 @@ config_file_data() ->
%% is expected to exist. %% is expected to exist.
%% @end %% @end
credentials_file() -> credentials_file() ->
credentials_file(os:getenv("AWS_SHARED_CREDENTIALS_FILE")). credentials_file(os:getenv("AWS_SHARED_CREDENTIALS_FILE")).
-spec credentials_file(Path :: false | string()) -> string(). -spec credentials_file(Path :: false | string()) -> string().
%% @doc Return the shared credentials file to test using either the value of the %% @doc Return the shared credentials file to test using either the value of the
@ -245,25 +245,25 @@ credentials_file() ->
%% is expected to exist. %% is expected to exist.
%% @end %% @end
credentials_file(false) -> credentials_file(false) ->
filename:join([home_path(), ".aws", "credentials"]); filename:join([home_path(), ".aws", "credentials"]);
credentials_file(EnvVar) -> credentials_file(EnvVar) ->
EnvVar. EnvVar.
-spec credentials_file_data() -> list() | {error, Reason :: atom()}. -spec credentials_file_data() -> list() | {error, Reason :: atom()}.
%% @doc Return the values from a configuration file as a proplist by section %% @doc Return the values from a configuration file as a proplist by section
%% @end %% @end
credentials_file_data() -> credentials_file_data() ->
ini_file_data(credentials_file()). ini_file_data(credentials_file()).
-spec get_value
-spec get_value(Key :: atom(), Settings :: list()) -> any(); (Key :: atom(), Settings :: list()) -> any();
(Key :: atom(), {error, Reason :: atom()}) -> {error, Reason :: atom()}. (Key :: atom(), {error, Reason :: atom()}) -> {error, Reason :: atom()}.
%% @doc Get the value for a key from a settings proplist. %% @doc Get the value for a key from a settings proplist.
%% @end %% @end
get_value(Key, Settings) when is_list(Settings) -> get_value(Key, Settings) when is_list(Settings) ->
proplists:get_value(Key, Settings, {error, undefined}); proplists:get_value(Key, Settings, {error, undefined});
get_value(_, {error, Reason}) -> {error, Reason}. get_value(_, {error, Reason}) ->
{error, Reason}.
-spec home_path() -> string(). -spec home_path() -> string().
%% @doc Return the path to the current user's home directory, checking for the %% @doc Return the path to the current user's home directory, checking for the
@ -271,8 +271,7 @@ get_value(_, {error, Reason}) -> {error, Reason}.
%% directory if it's not set. %% directory if it's not set.
%% @end %% @end
home_path() -> home_path() ->
home_path(os:getenv("HOME")). home_path(os:getenv("HOME")).
-spec home_path(Value :: string() | false) -> string(). -spec home_path(Value :: string() | false) -> string().
%% @doc Return the path to the current user's home directory, checking for the %% @doc Return the path to the current user's home directory, checking for the
@ -282,404 +281,430 @@ home_path() ->
home_path(false) -> filename:absname("."); home_path(false) -> filename:absname(".");
home_path(Value) -> Value. home_path(Value) -> Value.
-spec ini_file_data(Path :: string()) ->
-spec ini_file_data(Path :: string()) list() | {error, atom()}.
-> list() | {error, atom()}.
%% @doc Return the parsed ini file for the specified path. %% @doc Return the parsed ini file for the specified path.
%% @end %% @end
ini_file_data(Path) -> ini_file_data(Path) ->
ini_file_data(Path, filelib:is_file(Path)). ini_file_data(Path, filelib:is_file(Path)).
-spec ini_file_data(Path :: string(), FileExists :: boolean()) ->
-spec ini_file_data(Path :: string(), FileExists :: boolean()) list() | {error, atom()}.
-> list() | {error, atom()}.
%% @doc Return the parsed ini file for the specified path. %% @doc Return the parsed ini file for the specified path.
%% @end %% @end
ini_file_data(Path, true) -> ini_file_data(Path, true) ->
case read_file(Path) of case read_file(Path) of
{ok, Lines} -> ini_parse_lines(Lines, none, none, []); {ok, Lines} -> ini_parse_lines(Lines, none, none, []);
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end; end;
ini_file_data(_, false) -> {error, enoent}. ini_file_data(_, false) ->
{error, enoent}.
-spec ini_format_key(any()) -> atom() | {error, type}. -spec ini_format_key(any()) -> atom() | {error, type}.
%% @doc Converts a ini file key to an atom, stripping any leading whitespace %% @doc Converts a ini file key to an atom, stripping any leading whitespace
%% @end %% @end
ini_format_key(Key) -> ini_format_key(Key) ->
case io_lib:printable_list(Key) of case io_lib:printable_list(Key) of
true -> list_to_atom(string:strip(Key)); true -> list_to_atom(string:strip(Key));
false -> {error, type} false -> {error, type}
end. end.
-spec ini_parse_line(
-spec ini_parse_line(Section :: list(), Section :: list(),
Key :: atom(), Key :: atom(),
Line :: binary()) Line :: binary()
-> {Section :: list(), Key :: string() | none}. ) ->
{Section :: list(), Key :: string() | none}.
%% @doc Parse the AWS configuration INI file, returning a proplist %% @doc Parse the AWS configuration INI file, returning a proplist
%% @end %% @end
ini_parse_line(Section, Parent, <<" ", Line/binary>>) -> ini_parse_line(Section, Parent, <<" ", Line/binary>>) ->
Child = proplists:get_value(Parent, Section, []), Child = proplists:get_value(Parent, Section, []),
{ok, NewChild} = ini_parse_line_parts(Child, ini_split_line(Line)), {ok, NewChild} = ini_parse_line_parts(Child, ini_split_line(Line)),
{lists:keystore(Parent, 1, Section, {Parent, NewChild}), Parent}; {lists:keystore(Parent, 1, Section, {Parent, NewChild}), Parent};
ini_parse_line(Section, _, Line) -> ini_parse_line(Section, _, Line) ->
case ini_parse_line_parts(Section, ini_split_line(Line)) of case ini_parse_line_parts(Section, ini_split_line(Line)) of
{ok, NewSection} -> {NewSection, none}; {ok, NewSection} -> {NewSection, none};
{new_parent, Parent} -> {Section, Parent} {new_parent, Parent} -> {Section, Parent}
end. end.
-spec ini_parse_line_parts(
-spec ini_parse_line_parts(Section :: list(), Section :: list(),
Parts :: list()) Parts :: list()
-> {ok, list()} | {new_parent, atom()}. ) ->
{ok, list()} | {new_parent, atom()}.
%% @doc Parse the AWS configuration INI file, returning a proplist %% @doc Parse the AWS configuration INI file, returning a proplist
%% @end %% @end
ini_parse_line_parts(Section, []) -> {ok, Section}; ini_parse_line_parts(Section, []) ->
{ok, Section};
ini_parse_line_parts(Section, [RawKey, Value]) -> ini_parse_line_parts(Section, [RawKey, Value]) ->
Key = ini_format_key(RawKey), Key = ini_format_key(RawKey),
{ok, lists:keystore(Key, 1, Section, {Key, maybe_convert_number(Value)})}; {ok, lists:keystore(Key, 1, Section, {Key, maybe_convert_number(Value)})};
ini_parse_line_parts(_, [RawKey]) -> ini_parse_line_parts(_, [RawKey]) ->
{new_parent, ini_format_key(RawKey)}. {new_parent, ini_format_key(RawKey)}.
-spec ini_parse_lines(
-spec ini_parse_lines(Lines::[binary()], Lines :: [binary()],
SectionName :: string() | atom(), SectionName :: string() | atom(),
Parent :: atom(), Parent :: atom(),
Accumulator :: list()) Accumulator :: list()
-> list(). ) ->
list().
%% @doc Parse the AWS configuration INI file %% @doc Parse the AWS configuration INI file
%% @end %% @end
ini_parse_lines([], _, _, Settings) -> Settings; ini_parse_lines([], _, _, Settings) ->
ini_parse_lines([H|T], SectionName, Parent, Settings) -> Settings;
{ok, NewSectionName} = ini_parse_section_name(SectionName, H), ini_parse_lines([H | T], SectionName, Parent, Settings) ->
{ok, NewParent, NewSettings} = ini_parse_section(H, NewSectionName, {ok, NewSectionName} = ini_parse_section_name(SectionName, H),
Parent, Settings), {ok, NewParent, NewSettings} = ini_parse_section(
ini_parse_lines(T, NewSectionName, NewParent, NewSettings). H,
NewSectionName,
Parent,
Settings
),
ini_parse_lines(T, NewSectionName, NewParent, NewSettings).
-spec ini_parse_section(
-spec ini_parse_section(Line :: binary(), Line :: binary(),
SectionName :: string(), SectionName :: string(),
Parent :: atom(), Parent :: atom(),
Section :: list()) Section :: list()
-> {ok, NewParent :: atom(), Section :: list()}. ) ->
{ok, NewParent :: atom(), Section :: list()}.
%% @doc Parse a line from the ini file, returning it as part of the appropriate %% @doc Parse a line from the ini file, returning it as part of the appropriate
%% section. %% section.
%% @end %% @end
ini_parse_section(Line, SectionName, Parent, Settings) -> ini_parse_section(Line, SectionName, Parent, Settings) ->
Section = proplists:get_value(SectionName, Settings, []), Section = proplists:get_value(SectionName, Settings, []),
{NewSection, NewParent} = ini_parse_line(Section, Parent, Line), {NewSection, NewParent} = ini_parse_line(Section, Parent, Line),
{ok, NewParent, lists:keystore(SectionName, 1, Settings, {ok, NewParent,
{SectionName, NewSection})}. lists:keystore(
SectionName,
1,
Settings,
{SectionName, NewSection}
)}.
-spec ini_parse_section_name(
-spec ini_parse_section_name(CurrentSection :: string() | atom(), CurrentSection :: string() | atom(),
Line :: binary()) Line :: binary()
-> {ok, SectionName :: string()}. ) ->
{ok, SectionName :: string()}.
%% @doc Attempts to parse a section name from the current line, returning either %% @doc Attempts to parse a section name from the current line, returning either
%% the new parsed section name, or the current section name. %% the new parsed section name, or the current section name.
%% @end %% @end
ini_parse_section_name(CurrentSection, Line) -> ini_parse_section_name(CurrentSection, Line) ->
Value = binary_to_list(Line), Value = binary_to_list(Line),
case re:run(Value, "\\[([\\w\\s+\\-_]+)\\]", [{capture, all, list}]) of case re:run(Value, "\\[([\\w\\s+\\-_]+)\\]", [{capture, all, list}]) of
{match, [_, SectionName]} -> {ok, SectionName}; {match, [_, SectionName]} -> {ok, SectionName};
nomatch -> {ok, CurrentSection} nomatch -> {ok, CurrentSection}
end. end.
-spec ini_split_line(binary()) -> list(). -spec ini_split_line(binary()) -> list().
%% @doc Split a key value pair delimited by ``=`` to a list of strings. %% @doc Split a key value pair delimited by ``=`` to a list of strings.
%% @end %% @end
ini_split_line(Line) -> ini_split_line(Line) ->
string:tokens(string:strip(binary_to_list(Line)), "="). string:tokens(string:strip(binary_to_list(Line)), "=").
-spec instance_availability_zone_url() -> string(). -spec instance_availability_zone_url() -> string().
%% @doc Return the URL for querying the availability zone from the Instance %% @doc Return the URL for querying the availability zone from the Instance
%% Metadata service %% Metadata service
%% @end %% @end
instance_availability_zone_url() -> instance_availability_zone_url() ->
instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_AZ], "/")). instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_AZ], "/")).
-spec instance_credentials_url(string()) -> string(). -spec instance_credentials_url(string()) -> string().
%% @doc Return the URL for querying temporary credentials from the Instance %% @doc Return the URL for querying temporary credentials from the Instance
%% Metadata service for the specified role %% Metadata service for the specified role
%% @end %% @end
instance_credentials_url(Role) -> instance_credentials_url(Role) ->
instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS, Role], "/")). instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS, Role], "/")).
-spec instance_metadata_url(string()) -> string(). -spec instance_metadata_url(string()) -> string().
%% @doc Build the Instance Metadata service URL for the specified path %% @doc Build the Instance Metadata service URL for the specified path
%% @end %% @end
instance_metadata_url(Path) -> instance_metadata_url(Path) ->
rabbitmq_aws_urilib:build(#uri{scheme = http, rabbitmq_aws_urilib:build(#uri{
authority = {undefined, ?INSTANCE_HOST, undefined}, scheme = http,
path = Path, query = []}). authority = {undefined, ?INSTANCE_HOST, undefined},
path = Path,
query = []
}).
-spec instance_role_url() -> string(). -spec instance_role_url() -> string().
%% @doc Return the URL for querying the role associated with the current %% @doc Return the URL for querying the role associated with the current
%% instance from the Instance Metadata service %% instance from the Instance Metadata service
%% @end %% @end
instance_role_url() -> instance_role_url() ->
instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS], "/")). instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS], "/")).
-spec imdsv2_token_url() -> string(). -spec imdsv2_token_url() -> string().
%% @doc Return the URL for obtaining EC2 IMDSv2 token from the Instance Metadata service. %% @doc Return the URL for obtaining EC2 IMDSv2 token from the Instance Metadata service.
%% @end %% @end
imdsv2_token_url() -> imdsv2_token_url() ->
instance_metadata_url(?TOKEN_URL). instance_metadata_url(?TOKEN_URL).
-spec instance_id_url() -> string(). -spec instance_id_url() -> string().
%% @doc Return the URL for querying the id of the current instance from the Instance Metadata service. %% @doc Return the URL for querying the id of the current instance from the Instance Metadata service.
%% @end %% @end
instance_id_url() -> instance_id_url() ->
instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_ID], "/")). instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_ID], "/")).
-spec lookup_credentials(
-spec lookup_credentials(Profile :: string(), Profile :: string(),
AccessKey :: string() | false, AccessKey :: string() | false,
SecretKey :: string() | false) SecretKey :: string() | false
-> security_credentials(). ) ->
security_credentials().
%% @doc Return the access key and secret access key if they are set in %% @doc Return the access key and secret access key if they are set in
%% environment variables, otherwise lookup the credentials from the config %% environment variables, otherwise lookup the credentials from the config
%% file for the specified profile. %% file for the specified profile.
%% @end %% @end
lookup_credentials(Profile, false, _) -> lookup_credentials(Profile, false, _) ->
lookup_credentials_from_config(Profile, lookup_credentials_from_config(
value(Profile, aws_access_key_id), Profile,
value(Profile, aws_secret_access_key)); value(Profile, aws_access_key_id),
value(Profile, aws_secret_access_key)
);
lookup_credentials(Profile, _, false) -> lookup_credentials(Profile, _, false) ->
lookup_credentials_from_config(Profile, lookup_credentials_from_config(
value(Profile, aws_access_key_id), Profile,
value(Profile, aws_secret_access_key)); value(Profile, aws_access_key_id),
value(Profile, aws_secret_access_key)
);
lookup_credentials(_, AccessKey, SecretKey) -> lookup_credentials(_, AccessKey, SecretKey) ->
{ok, AccessKey, SecretKey, undefined, undefined}. {ok, AccessKey, SecretKey, undefined, undefined}.
-spec lookup_credentials_from_config(
-spec lookup_credentials_from_config(Profile :: string(), Profile :: string(),
access_key() | {error, Reason :: atom()}, access_key() | {error, Reason :: atom()},
secret_access_key()| {error, Reason :: atom()}) secret_access_key() | {error, Reason :: atom()}
-> security_credentials(). ) ->
security_credentials().
%% @doc Return the access key and secret access key if they are set in %% @doc Return the access key and secret access key if they are set in
%% for the specified profile in the config file, if it exists. If it does %% for the specified profile in the config file, if it exists. If it does
%% not exist or the profile is not set or the values are not set in the %% not exist or the profile is not set or the values are not set in the
%% profile, look up the values in the shared credentials file %% profile, look up the values in the shared credentials file
%% @end %% @end
lookup_credentials_from_config(Profile, {error,_}, _) -> lookup_credentials_from_config(Profile, {error, _}, _) ->
lookup_credentials_from_file(Profile, credentials_file_data()); lookup_credentials_from_file(Profile, credentials_file_data());
lookup_credentials_from_config(_, AccessKey, SecretKey) -> lookup_credentials_from_config(_, AccessKey, SecretKey) ->
{ok, AccessKey, SecretKey, undefined, undefined}. {ok, AccessKey, SecretKey, undefined, undefined}.
-spec lookup_credentials_from_file(
-spec lookup_credentials_from_file(Profile :: string(), Profile :: string(),
Credentials :: list()) Credentials :: list()
-> security_credentials(). ) ->
security_credentials().
%% @doc Check to see if the shared credentials file exists and if it does, %% @doc Check to see if the shared credentials file exists and if it does,
%% invoke ``lookup_credentials_from_shared_creds_section/2`` to attempt to %% invoke ``lookup_credentials_from_shared_creds_section/2`` to attempt to
%% get the credentials values out of it. If the file does not exist, %% get the credentials values out of it. If the file does not exist,
%% attempt to lookup the values from the EC2 instance metadata service. %% attempt to lookup the values from the EC2 instance metadata service.
%% @end %% @end
lookup_credentials_from_file(_, {error,_}) -> lookup_credentials_from_file(_, {error, _}) ->
lookup_credentials_from_instance_metadata(); lookup_credentials_from_instance_metadata();
lookup_credentials_from_file(Profile, Credentials) -> lookup_credentials_from_file(Profile, Credentials) ->
Section = proplists:get_value(Profile, Credentials), Section = proplists:get_value(Profile, Credentials),
lookup_credentials_from_section(Section). lookup_credentials_from_section(Section).
-spec lookup_credentials_from_section(Credentials :: list() | undefined) ->
-spec lookup_credentials_from_section(Credentials :: list() | undefined) security_credentials().
-> security_credentials().
%% @doc Return the access key and secret access key if they are set in %% @doc Return the access key and secret access key if they are set in
%% for the specified profile from the shared credentials file. If the %% for the specified profile from the shared credentials file. If the
%% profile is not set or the values are not set in the profile, attempt to %% profile is not set or the values are not set in the profile, attempt to
%% lookup the values from the EC2 instance metadata service. %% lookup the values from the EC2 instance metadata service.
%% @end %% @end
lookup_credentials_from_section(undefined) -> lookup_credentials_from_section(undefined) ->
lookup_credentials_from_instance_metadata(); lookup_credentials_from_instance_metadata();
lookup_credentials_from_section(Credentials) -> lookup_credentials_from_section(Credentials) ->
AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined), AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined),
SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined), SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined),
lookup_credentials_from_proplist(AccessKey, SecretKey). lookup_credentials_from_proplist(AccessKey, SecretKey).
-spec lookup_credentials_from_proplist(
-spec lookup_credentials_from_proplist(AccessKey :: access_key(), AccessKey :: access_key(),
SecretAccessKey :: secret_access_key()) SecretAccessKey :: secret_access_key()
-> security_credentials(). ) ->
security_credentials().
%% @doc Process the contents of the Credentials proplists checking if the %% @doc Process the contents of the Credentials proplists checking if the
%% access key and secret access key are both set. %% access key and secret access key are both set.
%% @end %% @end
lookup_credentials_from_proplist(undefined, _) -> lookup_credentials_from_proplist(undefined, _) ->
lookup_credentials_from_instance_metadata(); lookup_credentials_from_instance_metadata();
lookup_credentials_from_proplist(_, undefined) -> lookup_credentials_from_proplist(_, undefined) ->
lookup_credentials_from_instance_metadata(); lookup_credentials_from_instance_metadata();
lookup_credentials_from_proplist(AccessKey, SecretKey) -> lookup_credentials_from_proplist(AccessKey, SecretKey) ->
{ok, AccessKey, SecretKey, undefined, undefined}. {ok, AccessKey, SecretKey, undefined, undefined}.
-spec lookup_credentials_from_instance_metadata() ->
-spec lookup_credentials_from_instance_metadata() security_credentials().
-> security_credentials().
%% @spec lookup_credentials_from_instance_metadata() -> Result. %% @spec lookup_credentials_from_instance_metadata() -> Result.
%% @doc Attempt to lookup the values from the EC2 instance metadata service. %% @doc Attempt to lookup the values from the EC2 instance metadata service.
%% @end %% @end
lookup_credentials_from_instance_metadata() -> lookup_credentials_from_instance_metadata() ->
Role = maybe_get_role_from_instance_metadata(), Role = maybe_get_role_from_instance_metadata(),
maybe_get_credentials_from_instance_metadata(Role). maybe_get_credentials_from_instance_metadata(Role).
-spec lookup_region(
-spec lookup_region(Profile :: string(), Profile :: string(),
Region :: false | string()) Region :: false | string()
-> {ok, string()} | {error, undefined}. ) ->
{ok, string()} | {error, undefined}.
%% @doc If Region is false, lookup the region from the config or the EC2 %% @doc If Region is false, lookup the region from the config or the EC2
%% instance metadata service. %% instance metadata service.
%% @end %% @end
lookup_region(Profile, false) -> lookup_region(Profile, false) ->
lookup_region_from_config(values(Profile)); lookup_region_from_config(values(Profile));
lookup_region(_, Region) -> {ok, Region}. lookup_region(_, Region) ->
{ok, Region}.
-spec lookup_region_from_config(Settings :: list() | {error, atom()}) ->
-spec lookup_region_from_config(Settings :: list() | {error, atom()}) {ok, string()} | {error, undefined}.
-> {ok, string()} | {error, undefined}.
%% @doc Return the region from the local configuration file. If local config %% @doc Return the region from the local configuration file. If local config
%% settings are not found, try to lookup the region from the EC2 instance %% settings are not found, try to lookup the region from the EC2 instance
%% metadata service. %% metadata service.
%% @end %% @end
lookup_region_from_config({error, _}) -> lookup_region_from_config({error, _}) ->
maybe_get_region_from_instance_metadata(); maybe_get_region_from_instance_metadata();
lookup_region_from_config(Settings) -> lookup_region_from_config(Settings) ->
lookup_region_from_settings(proplists:get_value(region, Settings)). lookup_region_from_settings(proplists:get_value(region, Settings)).
-spec lookup_region_from_settings(any() | undefined) ->
-spec lookup_region_from_settings(any() | undefined) {ok, string()} | {error, undefined}.
-> {ok, string()} | {error, undefined}.
%% @doc Decide if the region should be loaded from the Instance Metadata service %% @doc Decide if the region should be loaded from the Instance Metadata service
%% of if it's already set. %% of if it's already set.
%% @end %% @end
lookup_region_from_settings(undefined) -> lookup_region_from_settings(undefined) ->
maybe_get_region_from_instance_metadata(); maybe_get_region_from_instance_metadata();
lookup_region_from_settings(Region) -> lookup_region_from_settings(Region) ->
{ok, Region}. {ok, Region}.
-spec maybe_convert_number(string()) -> integer() | float(). -spec maybe_convert_number(string()) -> integer() | float().
%% @doc Returns an integer or float from a string if possible, otherwise %% @doc Returns an integer or float from a string if possible, otherwise
%% returns the string(). %% returns the string().
%% @end %% @end
maybe_convert_number(Value) -> maybe_convert_number(Value) ->
Stripped = string:strip(Value), Stripped = string:strip(Value),
case string:to_float(Stripped) of case string:to_float(Stripped) of
{error,no_float} -> {error, no_float} ->
try try
list_to_integer(Stripped) list_to_integer(Stripped)
catch catch
error:badarg -> Stripped error:badarg -> Stripped
end; end;
{F,_Rest} -> F {F, _Rest} ->
end. F
end.
-spec maybe_get_credentials_from_instance_metadata(
-spec maybe_get_credentials_from_instance_metadata({ok, Role :: string()} | {ok, Role :: string()}
{error, undefined}) | {error, undefined}
-> {'ok', security_credentials()} | {'error', term()}. ) ->
{'ok', security_credentials()} | {'error', term()}.
%% @doc Try to query the EC2 local instance metadata service to get temporary %% @doc Try to query the EC2 local instance metadata service to get temporary
%% authentication credentials. %% authentication credentials.
%% @end %% @end
maybe_get_credentials_from_instance_metadata({error, undefined}) -> maybe_get_credentials_from_instance_metadata({error, undefined}) ->
{error, undefined}; {error, undefined};
maybe_get_credentials_from_instance_metadata({ok, Role}) -> maybe_get_credentials_from_instance_metadata({ok, Role}) ->
URL = instance_credentials_url(Role), URL = instance_credentials_url(Role),
parse_credentials_response(perform_http_get_instance_metadata(URL)). parse_credentials_response(perform_http_get_instance_metadata(URL)).
-spec maybe_get_region_from_instance_metadata() ->
-spec maybe_get_region_from_instance_metadata() {ok, Region :: string()} | {error, Reason :: atom()}.
-> {ok, Region :: string()} | {error, Reason :: atom()}.
%% @doc Try to query the EC2 local instance metadata service to get the region %% @doc Try to query the EC2 local instance metadata service to get the region
%% @end %% @end
maybe_get_region_from_instance_metadata() -> maybe_get_region_from_instance_metadata() ->
URL = instance_availability_zone_url(), URL = instance_availability_zone_url(),
parse_az_response(perform_http_get_instance_metadata(URL)). parse_az_response(perform_http_get_instance_metadata(URL)).
%% @doc Try to query the EC2 local instance metadata service to get the role %% @doc Try to query the EC2 local instance metadata service to get the role
%% assigned to the instance. %% assigned to the instance.
%% @end %% @end
maybe_get_role_from_instance_metadata() -> maybe_get_role_from_instance_metadata() ->
URL = instance_role_url(), URL = instance_role_url(),
parse_body_response(perform_http_get_instance_metadata(URL)). parse_body_response(perform_http_get_instance_metadata(URL)).
-spec parse_az_response(httpc_result()) ->
-spec parse_az_response(httpc_result()) {ok, Region :: string()} | {error, Reason :: atom()}.
-> {ok, Region :: string()} | {error, Reason :: atom()}.
%% @doc Parse the response from the Availability Zone query to the %% @doc Parse the response from the Availability Zone query to the
%% Instance Metadata service, returning the Region if successful. %% Instance Metadata service, returning the Region if successful.
%% end. %% end.
parse_az_response({error, _}) -> {error, undefined}; parse_az_response({error, _}) -> {error, undefined};
parse_az_response({ok, {{_, 200, _}, _, Body}}) parse_az_response({ok, {{_, 200, _}, _, Body}}) -> {ok, region_from_availability_zone(Body)};
-> {ok, region_from_availability_zone(Body)};
parse_az_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}. parse_az_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}.
-spec parse_body_response(httpc_result()) ->
-spec parse_body_response(httpc_result()) {ok, Value :: string()} | {error, Reason :: atom()}.
-> {ok, Value :: string()} | {error, Reason :: atom()}.
%% @doc Parse the return response from the Instance Metadata Service where the %% @doc Parse the return response from the Instance Metadata Service where the
%% body value is the string to process. %% body value is the string to process.
%% end. %% end.
parse_body_response({error, _}) -> {error, undefined}; parse_body_response({error, _}) ->
parse_body_response({ok, {{_, 200, _}, _, Body}}) -> {ok, Body}; {error, undefined};
parse_body_response({ok, {{_, 200, _}, _, Body}}) ->
{ok, Body};
parse_body_response({ok, {{_, 401, _}, _, _}}) -> parse_body_response({ok, {{_, 401, _}, _, _}}) ->
?LOG_ERROR(get_instruction_on_instance_metadata_error("Unauthorized instance metadata service request.")), ?LOG_ERROR(
{error, undefined}; get_instruction_on_instance_metadata_error(
"Unauthorized instance metadata service request."
)
),
{error, undefined};
parse_body_response({ok, {{_, 403, _}, _, _}}) -> parse_body_response({ok, {{_, 403, _}, _, _}}) ->
?LOG_ERROR(get_instruction_on_instance_metadata_error("The request is not allowed or the instance metadata service is turned off.")), ?LOG_ERROR(
{error, undefined}; get_instruction_on_instance_metadata_error(
parse_body_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}. "The request is not allowed or the instance metadata service is turned off."
)
),
{error, undefined};
parse_body_response({ok, {{_, _, _}, _, _}}) ->
{error, undefined}.
-spec parse_credentials_response(httpc_result()) -> security_credentials(). -spec parse_credentials_response(httpc_result()) -> security_credentials().
%% @doc Try to query the EC2 local instance metadata service to get the role %% @doc Try to query the EC2 local instance metadata service to get the role
%% assigned to the instance. %% assigned to the instance.
%% @end %% @end
parse_credentials_response({error, _}) -> {error, undefined}; parse_credentials_response({error, _}) ->
parse_credentials_response({ok, {{_, 404, _}, _, _}}) -> {error, undefined}; {error, undefined};
parse_credentials_response({ok, {{_, 404, _}, _, _}}) ->
{error, undefined};
parse_credentials_response({ok, {{_, 200, _}, _, Body}}) -> parse_credentials_response({ok, {{_, 200, _}, _, Body}}) ->
Parsed = rabbitmq_aws_json:decode(Body), Parsed = rabbitmq_aws_json:decode(Body),
{ok, {ok, proplists:get_value("AccessKeyId", Parsed), proplists:get_value("SecretAccessKey", Parsed),
proplists:get_value("AccessKeyId", Parsed), parse_iso8601_timestamp(proplists:get_value("Expiration", Parsed)),
proplists:get_value("SecretAccessKey", Parsed), proplists:get_value("Token", Parsed)}.
parse_iso8601_timestamp(proplists:get_value("Expiration", Parsed)),
proplists:get_value("Token", Parsed)}.
-spec perform_http_get_instance_metadata(string()) -> httpc_result(). -spec perform_http_get_instance_metadata(string()) -> httpc_result().
%% @doc Wrap httpc:get/4 to simplify Instance Metadata service v2 requests %% @doc Wrap httpc:get/4 to simplify Instance Metadata service v2 requests
%% @end %% @end
perform_http_get_instance_metadata(URL) -> perform_http_get_instance_metadata(URL) ->
?LOG_DEBUG("Querying instance metadata service: ~tp", [URL]), ?LOG_DEBUG("Querying instance metadata service: ~tp", [URL]),
httpc:request(get, {URL, instance_metadata_request_headers()}, httpc:request(
[{timeout, ?DEFAULT_HTTP_TIMEOUT}], []). get,
{URL, instance_metadata_request_headers()},
[{timeout, ?DEFAULT_HTTP_TIMEOUT}],
[]
).
-spec get_instruction_on_instance_metadata_error(string()) -> string(). -spec get_instruction_on_instance_metadata_error(string()) -> string().
%% @doc Return error message on failures related to EC2 Instance Metadata Service with a reference to AWS document. %% @doc Return error message on failures related to EC2 Instance Metadata Service with a reference to AWS document.
%% end %% end
get_instruction_on_instance_metadata_error(ErrorMessage) -> get_instruction_on_instance_metadata_error(ErrorMessage) ->
ErrorMessage ++ ErrorMessage ++
" Please refer to the AWS documentation for details on how to configure the instance metadata service: " " Please refer to the AWS documentation for details on how to configure the instance metadata service: "
"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html.". "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html.".
-spec parse_iso8601_timestamp(Timestamp :: string() | binary()) -> calendar:datetime(). -spec parse_iso8601_timestamp(Timestamp :: string() | binary()) -> calendar:datetime().
%% @doc Parse a ISO8601 timestamp, returning a datetime() value. %% @doc Parse a ISO8601 timestamp, returning a datetime() value.
%% @end %% @end
parse_iso8601_timestamp(Timestamp) when is_binary(Timestamp) -> parse_iso8601_timestamp(Timestamp) when is_binary(Timestamp) ->
parse_iso8601_timestamp(binary_to_list(Timestamp)); parse_iso8601_timestamp(binary_to_list(Timestamp));
parse_iso8601_timestamp(Timestamp) -> parse_iso8601_timestamp(Timestamp) ->
[Date, Time] = string:tokens(Timestamp, "T"), [Date, Time] = string:tokens(Timestamp, "T"),
[Year, Month, Day] = string:tokens(Date, "-"), [Year, Month, Day] = string:tokens(Date, "-"),
[Hour, Minute, Second] = string:tokens(Time, ":"), [Hour, Minute, Second] = string:tokens(Time, ":"),
{{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {
{list_to_integer(Hour), list_to_integer(Minute), list_to_integer(string:left(Second,2))}}. list_to_integer(Hour), list_to_integer(Minute), list_to_integer(string:left(Second, 2))
}}.
-spec profile() -> string(). -spec profile() -> string().
%% @doc Return the value of the AWS_DEFAULT_PROFILE environment variable or the %% @doc Return the value of the AWS_DEFAULT_PROFILE environment variable or the
@ -687,7 +712,6 @@ parse_iso8601_timestamp(Timestamp) ->
%% @end %% @end
profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")). profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")).
-spec profile(false | string()) -> string(). -spec profile(false | string()) -> string().
%% @doc Process the value passed in to determine if we will return the default %% @doc Process the value passed in to determine if we will return the default
%% profile or the value from the environment variable. %% profile or the value from the environment variable.
@ -695,7 +719,6 @@ profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")).
profile(false) -> ?DEFAULT_PROFILE; profile(false) -> ?DEFAULT_PROFILE;
profile(Value) -> Value. profile(Value) -> Value.
-spec read_file(string()) -> {'ok', [binary()]} | {error, Reason :: atom()}. -spec read_file(string()) -> {'ok', [binary()]} | {error, Reason :: atom()}.
%% @doc Read the specified file, returning the contents as a list of strings. %% @doc Read the specified file, returning the contents as a list of strings.
%% @end %% @end
@ -703,54 +726,67 @@ read_file(Path) ->
case file:read_file(Path) of case file:read_file(Path) of
{ok, Binary} -> {ok, Binary} ->
{ok, re:split(Binary, <<"\r\n|\n">>, [{return, binary}])}; {ok, re:split(Binary, <<"\r\n|\n">>, [{return, binary}])};
{error, _} = Error -> Error {error, _} = Error ->
Error
end. end.
-spec region_from_availability_zone(Value :: string()) -> string(). -spec region_from_availability_zone(Value :: string()) -> string().
%% @doc Strip the availability zone suffix from the region. %% @doc Strip the availability zone suffix from the region.
%% @end %% @end
region_from_availability_zone(Value) -> region_from_availability_zone(Value) ->
string:sub_string(Value, 1, length(Value) - 1). string:sub_string(Value, 1, length(Value) - 1).
-spec load_imdsv2_token() -> security_token(). -spec load_imdsv2_token() -> security_token().
%% @doc Attempt to obtain EC2 IMDSv2 token. %% @doc Attempt to obtain EC2 IMDSv2 token.
%% @end %% @end
load_imdsv2_token() -> load_imdsv2_token() ->
TokenUrl = imdsv2_token_url(), TokenUrl = imdsv2_token_url(),
?LOG_INFO("Attempting to obtain EC2 IMDSv2 token from ~tp ...", [TokenUrl]), ?LOG_INFO("Attempting to obtain EC2 IMDSv2 token from ~tp ...", [TokenUrl]),
case httpc:request(put, {TokenUrl, [{?METADATA_TOKEN_TTL_HEADER, integer_to_list(?METADATA_TOKEN_TTL_SECONDS)}]}, case
[{timeout, ?DEFAULT_HTTP_TIMEOUT}], []) of httpc:request(
{ok, {{_, 200, _}, _, Value}} -> put,
?LOG_DEBUG("Successfully obtained EC2 IMDSv2 token."), {TokenUrl, [{?METADATA_TOKEN_TTL_HEADER, integer_to_list(?METADATA_TOKEN_TTL_SECONDS)}]},
Value; [{timeout, ?DEFAULT_HTTP_TIMEOUT}],
{error, {{_, 400, _}, _, _}} -> []
?LOG_WARNING("Failed to obtain EC2 IMDSv2 token: Missing or Invalid Parameters The PUT request is not valid."), )
undefined; of
Other -> {ok, {{_, 200, _}, _, Value}} ->
?LOG_WARNING( ?LOG_DEBUG("Successfully obtained EC2 IMDSv2 token."),
get_instruction_on_instance_metadata_error("Failed to obtain EC2 IMDSv2 token: ~tp. " Value;
"Falling back to EC2 IMDSv1 for now. It is recommended to use EC2 IMDSv2."), [Other]), {error, {{_, 400, _}, _, _}} ->
undefined ?LOG_WARNING(
end. "Failed to obtain EC2 IMDSv2 token: Missing or Invalid Parameters The PUT request is not valid."
),
undefined;
Other ->
?LOG_WARNING(
get_instruction_on_instance_metadata_error(
"Failed to obtain EC2 IMDSv2 token: ~tp. "
"Falling back to EC2 IMDSv1 for now. It is recommended to use EC2 IMDSv2."
),
[Other]
),
undefined
end.
-spec instance_metadata_request_headers() -> headers(). -spec instance_metadata_request_headers() -> headers().
%% @doc Return headers used for instance metadata service requests. %% @doc Return headers used for instance metadata service requests.
%% @end %% @end
instance_metadata_request_headers() -> instance_metadata_request_headers() ->
case application:get_env(rabbit, aws_prefer_imdsv2) of case application:get_env(rabbit, aws_prefer_imdsv2) of
{ok, false} -> []; {ok, false} ->
_ -> %% undefined or {ok, true} [];
?LOG_DEBUG("EC2 Instance Metadata Service v2 (IMDSv2) is preferred."), %% undefined or {ok, true}
maybe_imdsv2_token_headers() _ ->
end. ?LOG_DEBUG("EC2 Instance Metadata Service v2 (IMDSv2) is preferred."),
maybe_imdsv2_token_headers()
end.
-spec maybe_imdsv2_token_headers() -> headers(). -spec maybe_imdsv2_token_headers() -> headers().
%% @doc Construct http request headers from Imdsv2Token to use with GET requests submitted to the EC2 Instance Metadata Service. %% @doc Construct http request headers from Imdsv2Token to use with GET requests submitted to the EC2 Instance Metadata Service.
%% @end %% @end
maybe_imdsv2_token_headers() -> maybe_imdsv2_token_headers() ->
case rabbitmq_aws:ensure_imdsv2_token_valid() of case rabbitmq_aws:ensure_imdsv2_token_valid() of
undefined -> []; undefined -> [];
Value -> [{?METADATA_TOKEN, Value}] Value -> [{?METADATA_TOKEN, Value}]
end. end.

View File

@ -13,46 +13,50 @@
%% @doc Decode a JSON string returning a proplist %% @doc Decode a JSON string returning a proplist
%% @end %% @end
decode(Value) when is_list(Value) -> decode(Value) when is_list(Value) ->
decode(list_to_binary(Value)); decode(list_to_binary(Value));
decode(<<>>) -> decode(<<>>) ->
[]; [];
decode(Value) when is_binary(Value) -> decode(Value) when is_binary(Value) ->
Decoded0 = rabbit_json:decode(Value), Decoded0 = rabbit_json:decode(Value),
Decoded = maps:to_list(Decoded0), Decoded = maps:to_list(Decoded0),
convert_binary_values(Decoded, []). convert_binary_values(Decoded, []).
-spec convert_binary_values(Value :: list(), Accumulator :: list()) -> list(). -spec convert_binary_values(Value :: list(), Accumulator :: list()) -> list().
%% @doc Convert the binary key/value pairs returned by rabbit_json to strings. %% @doc Convert the binary key/value pairs returned by rabbit_json to strings.
%% @end %% @end
convert_binary_values([], Value) -> Value; convert_binary_values([], Value) ->
convert_binary_values([{K, V}|T], Accum) when is_map(V) -> Value;
convert_binary_values( convert_binary_values([{K, V} | T], Accum) when is_map(V) ->
T, convert_binary_values(
lists:append( T,
Accum, lists:append(
[{binary_to_list(K), convert_binary_values(maps:to_list(V), [])}])); Accum,
convert_binary_values([{K, V}|T], Accum) when is_list(V) -> [{binary_to_list(K), convert_binary_values(maps:to_list(V), [])}]
convert_binary_values( )
T, );
lists:append( convert_binary_values([{K, V} | T], Accum) when is_list(V) ->
Accum, convert_binary_values(
[{binary_to_list(K), convert_binary_values(V, [])}])); T,
convert_binary_values([{}|T],Accum) -> lists:append(
convert_binary_values(T, [{} | Accum]); Accum,
convert_binary_values([{K, V}|T], Accum) when is_binary(V) -> [{binary_to_list(K), convert_binary_values(V, [])}]
convert_binary_values(T, lists:append(Accum, [{binary_to_list(K), binary_to_list(V)}])); )
convert_binary_values([{K, V}|T], Accum) -> );
convert_binary_values(T, lists:append(Accum, [{binary_to_list(K), V}])); convert_binary_values([{} | T], Accum) ->
convert_binary_values([M|T],Accum) when is_map(M) andalso map_size(M) =:= 0 -> convert_binary_values(T, [{} | Accum]);
convert_binary_values(T, [{} | Accum]); convert_binary_values([{K, V} | T], Accum) when is_binary(V) ->
convert_binary_values([H|T], Accum) when is_map(H) -> convert_binary_values(T, lists:append(Accum, [{binary_to_list(K), binary_to_list(V)}]));
convert_binary_values(T, lists:append(Accum, convert_binary_values(maps:to_list(H), []))); convert_binary_values([{K, V} | T], Accum) ->
convert_binary_values([H|T], Accum) when is_binary(H) -> convert_binary_values(T, lists:append(Accum, [{binary_to_list(K), V}]));
convert_binary_values(T, lists:append(Accum, [binary_to_list(H)])); convert_binary_values([M | T], Accum) when is_map(M) andalso map_size(M) =:= 0 ->
convert_binary_values([H|T], Accum) when is_integer(H) -> convert_binary_values(T, [{} | Accum]);
convert_binary_values(T, lists:append(Accum, [H])); convert_binary_values([H | T], Accum) when is_map(H) ->
convert_binary_values([H|T], Accum) when is_atom(H) -> convert_binary_values(T, lists:append(Accum, convert_binary_values(maps:to_list(H), [])));
convert_binary_values(T, lists:append(Accum, [H])); convert_binary_values([H | T], Accum) when is_binary(H) ->
convert_binary_values([H|T], Accum) -> convert_binary_values(T, lists:append(Accum, [binary_to_list(H)]));
convert_binary_values(T, lists:append(Accum, convert_binary_values(H, []))). convert_binary_values([H | T], Accum) when is_integer(H) ->
convert_binary_values(T, lists:append(Accum, [H]));
convert_binary_values([H | T], Accum) when is_atom(H) ->
convert_binary_values(T, lists:append(Accum, [H]));
convert_binary_values([H | T], Accum) ->
convert_binary_values(T, lists:append(Accum, convert_binary_values(H, []))).

View File

@ -24,260 +24,292 @@
%% @doc Create the signed request headers %% @doc Create the signed request headers
%% end %% end
headers(Request) -> headers(Request) ->
RequestTimestamp = local_time(), RequestTimestamp = local_time(),
PayloadHash = sha256(Request#request.body), PayloadHash = sha256(Request#request.body),
URI = rabbitmq_aws_urilib:parse(Request#request.uri), URI = rabbitmq_aws_urilib:parse(Request#request.uri),
{_, Host, _} = URI#uri.authority, {_, Host, _} = URI#uri.authority,
Headers = append_headers(RequestTimestamp, Headers = append_headers(
length(Request#request.body), RequestTimestamp,
PayloadHash, length(Request#request.body),
Host, PayloadHash,
Request#request.security_token, Host,
Request#request.headers), Request#request.security_token,
RequestHash = request_hash(Request#request.method, Request#request.headers
URI#uri.path, ),
URI#uri.query, RequestHash = request_hash(
Headers, Request#request.method,
Request#request.body), URI#uri.path,
AuthValue = authorization(Request#request.access_key, URI#uri.query,
Request#request.secret_access_key, Headers,
RequestTimestamp, Request#request.body
Request#request.region, ),
Request#request.service, AuthValue = authorization(
Headers, Request#request.access_key,
RequestHash), Request#request.secret_access_key,
sort_headers(lists:merge([{"authorization", AuthValue}], Headers)). RequestTimestamp,
Request#request.region,
Request#request.service,
Headers,
RequestHash
),
sort_headers(lists:merge([{"authorization", AuthValue}], Headers)).
-spec amz_date(AMZTimestamp :: string()) -> string(). -spec amz_date(AMZTimestamp :: string()) -> string().
%% @doc Extract the date from the AMZ timestamp format. %% @doc Extract the date from the AMZ timestamp format.
%% @end %% @end
amz_date(AMZTimestamp) -> amz_date(AMZTimestamp) ->
[RequestDate, _] = string:tokens(AMZTimestamp, "T"), [RequestDate, _] = string:tokens(AMZTimestamp, "T"),
RequestDate. RequestDate.
-spec append_headers(
-spec append_headers(AMZDate :: string(), AMZDate :: string(),
ContentLength :: integer(), ContentLength :: integer(),
PayloadHash :: string(), PayloadHash :: string(),
Hostname :: host(), Hostname :: host(),
SecurityToken :: security_token(), SecurityToken :: security_token(),
Headers :: headers()) -> list(). Headers :: headers()
) -> list().
%% @doc Append the headers that need to be signed to the headers passed in with %% @doc Append the headers that need to be signed to the headers passed in with
%% the request %% the request
%% @end %% @end
append_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken, Headers) -> append_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken, Headers) ->
Defaults = default_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken), Defaults = default_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken),
Headers1 = [{string:to_lower(Key), Value} || {Key, Value} <- Headers], Headers1 = [{string:to_lower(Key), Value} || {Key, Value} <- Headers],
Keys = lists:usort(lists:append([string:to_lower(Key) || {Key, _} <- Defaults], Keys = lists:usort(
[Key || {Key, _} <- Headers1])), lists:append(
sort_headers([{Key, header_value(Key, Headers1, proplists:get_value(Key, Defaults))} || Key <- Keys]). [string:to_lower(Key) || {Key, _} <- Defaults],
[Key || {Key, _} <- Headers1]
)
),
sort_headers([
{Key, header_value(Key, Headers1, proplists:get_value(Key, Defaults))}
|| Key <- Keys
]).
-spec authorization(
-spec authorization(AccessKey :: access_key(), AccessKey :: access_key(),
SecretAccessKey :: secret_access_key(), SecretAccessKey :: secret_access_key(),
RequestTimestamp :: string(), RequestTimestamp :: string(),
Region :: region(), Region :: region(),
Service :: string(), Service :: string(),
Headers :: headers(), Headers :: headers(),
RequestHash :: string()) -> string(). RequestHash :: string()
) -> string().
%% @doc Return the authorization header value %% @doc Return the authorization header value
%% @end %% @end
authorization(AccessKey, SecretAccessKey, RequestTimestamp, Region, Service, Headers, RequestHash) -> authorization(AccessKey, SecretAccessKey, RequestTimestamp, Region, Service, Headers, RequestHash) ->
RequestDate = amz_date(RequestTimestamp), RequestDate = amz_date(RequestTimestamp),
Scope = scope(RequestDate, Region, Service), Scope = scope(RequestDate, Region, Service),
Credentials = ?ALGORITHM ++ " Credential=" ++ AccessKey ++ "/" ++ Scope, Credentials = ?ALGORITHM ++ " Credential=" ++ AccessKey ++ "/" ++ Scope,
SignedHeaders = "SignedHeaders=" ++ signed_headers(Headers), SignedHeaders = "SignedHeaders=" ++ signed_headers(Headers),
StringToSign = string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash), StringToSign = string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash),
SigningKey = signing_key(SecretAccessKey, RequestDate, Region, Service), SigningKey = signing_key(SecretAccessKey, RequestDate, Region, Service),
Signature = string:join(["Signature", signature(StringToSign, SigningKey)], "="), Signature = string:join(["Signature", signature(StringToSign, SigningKey)], "="),
string:join([Credentials, SignedHeaders, Signature], ", "). string:join([Credentials, SignedHeaders, Signature], ", ").
-spec default_headers(
-spec default_headers(RequestTimestamp :: string(), RequestTimestamp :: string(),
ContentLength :: integer(), ContentLength :: integer(),
PayloadHash :: string(), PayloadHash :: string(),
Hostname :: host(), Hostname :: host(),
SecurityToken :: security_token()) -> headers(). SecurityToken :: security_token()
) -> headers().
%% @doc build the base headers that are merged in with the headers for every %% @doc build the base headers that are merged in with the headers for every
%% request. %% request.
%% @end %% @end
default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, undefined) -> default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, undefined) ->
[{"content-length", integer_to_list(ContentLength)}, [
{"date", RequestTimestamp}, {"content-length", integer_to_list(ContentLength)},
{"host", Hostname}, {"date", RequestTimestamp},
{"x-amz-content-sha256", PayloadHash}]; {"host", Hostname},
{"x-amz-content-sha256", PayloadHash}
];
default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, SecurityToken) -> default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, SecurityToken) ->
[{"content-length", integer_to_list(ContentLength)}, [
{"date", RequestTimestamp}, {"content-length", integer_to_list(ContentLength)},
{"host", Hostname}, {"date", RequestTimestamp},
{"x-amz-content-sha256", PayloadHash}, {"host", Hostname},
{"x-amz-security-token", SecurityToken}]. {"x-amz-content-sha256", PayloadHash},
{"x-amz-security-token", SecurityToken}
].
-spec canonical_headers(Headers :: headers()) -> string(). -spec canonical_headers(Headers :: headers()) -> string().
%% @doc Convert the headers list to a line-feed delimited string in the AWZ %% @doc Convert the headers list to a line-feed delimited string in the AWZ
%% canonical headers format. %% canonical headers format.
%% @end %% @end
canonical_headers(Headers) -> canonical_headers(Headers) ->
canonical_headers(sort_headers(Headers), []). canonical_headers(sort_headers(Headers), []).
-spec canonical_headers(Headers :: headers(), CanonicalHeaders :: list()) -> string(). -spec canonical_headers(Headers :: headers(), CanonicalHeaders :: list()) -> string().
%% @doc Convert the headers list to a line-feed delimited string in the AWZ %% @doc Convert the headers list to a line-feed delimited string in the AWZ
%% canonical headers format. %% canonical headers format.
%% @end %% @end
canonical_headers([], CanonicalHeaders) -> canonical_headers([], CanonicalHeaders) ->
lists:flatten(CanonicalHeaders); lists:flatten(CanonicalHeaders);
canonical_headers([{Key, Value}|T], CanonicalHeaders) -> canonical_headers([{Key, Value} | T], CanonicalHeaders) ->
Header = string:join([string:to_lower(Key), Value], ":") ++ "\n", Header = string:join([string:to_lower(Key), Value], ":") ++ "\n",
canonical_headers(T, lists:append(CanonicalHeaders, [Header])). canonical_headers(T, lists:append(CanonicalHeaders, [Header])).
-spec credential_scope(
-spec credential_scope(RequestDate :: string(), RequestDate :: string(),
Region :: region(), Region :: region(),
Service :: string()) -> string(). Service :: string()
) -> string().
%% @doc Return the credential scope string used in creating the request string to sign. %% @doc Return the credential scope string used in creating the request string to sign.
%% @end %% @end
credential_scope(RequestDate, Region, Service) -> credential_scope(RequestDate, Region, Service) ->
lists:flatten(string:join([RequestDate, Region, Service, "aws4_request"], "/")). lists:flatten(string:join([RequestDate, Region, Service, "aws4_request"], "/")).
-spec header_value(
-spec header_value(Key :: string(), Key :: string(),
Headers :: headers(), Headers :: headers(),
Default :: string()) -> string(). Default :: string()
) -> string().
%% @doc Return the the header value or the default value for the header if it %% @doc Return the the header value or the default value for the header if it
%% is not specified. %% is not specified.
%% @end %% @end
header_value(Key, Headers, Default) -> header_value(Key, Headers, Default) ->
proplists:get_value(Key, Headers, proplists:get_value(string:to_lower(Key), Headers, Default)). proplists:get_value(Key, Headers, proplists:get_value(string:to_lower(Key), Headers, Default)).
-spec hmac_sign(Key :: string(), Message :: string()) -> string(). -spec hmac_sign(Key :: string(), Message :: string()) -> string().
%% @doc Return the SHA-256 hash for the specified value. %% @doc Return the SHA-256 hash for the specified value.
%% @end %% @end
hmac_sign(Key, Message) -> hmac_sign(Key, Message) ->
SignedValue = crypto:mac(hmac, sha256, Key, Message), SignedValue = crypto:mac(hmac, sha256, Key, Message),
binary_to_list(SignedValue). binary_to_list(SignedValue).
-spec local_time() -> string(). -spec local_time() -> string().
%% @doc Return the current timestamp in GMT formatted in ISO8601 basic format. %% @doc Return the current timestamp in GMT formatted in ISO8601 basic format.
%% @end %% @end
local_time() -> local_time() ->
[LocalTime] = calendar:local_time_to_universal_time_dst(calendar:local_time()), [LocalTime] = calendar:local_time_to_universal_time_dst(calendar:local_time()),
local_time(LocalTime). local_time(LocalTime).
-spec local_time(calendar:datetime()) -> string(). -spec local_time(calendar:datetime()) -> string().
%% @doc Return the current timestamp in GMT formatted in ISO8601 basic format. %% @doc Return the current timestamp in GMT formatted in ISO8601 basic format.
%% @end %% @end
local_time({{Y,M,D},{HH,MM,SS}}) -> local_time({{Y, M, D}, {HH, MM, SS}}) ->
lists:flatten(io_lib:format(?ISOFORMAT_BASIC, [Y, M, D, HH, MM, SS])). lists:flatten(io_lib:format(?ISOFORMAT_BASIC, [Y, M, D, HH, MM, SS])).
-spec query_string(QueryArgs :: list()) -> string(). -spec query_string(QueryArgs :: list()) -> string().
%% @doc Return the sorted query string for the specified arguments. %% @doc Return the sorted query string for the specified arguments.
%% @end %% @end
query_string(undefined) -> ""; query_string(undefined) -> "";
query_string(QueryArgs) -> query_string(QueryArgs) -> rabbitmq_aws_urilib:build_query_string(lists:keysort(1, QueryArgs)).
rabbitmq_aws_urilib:build_query_string(lists:keysort(1, QueryArgs)).
-spec request_hash(
-spec request_hash(Method :: method(), Method :: method(),
Path :: path(), Path :: path(),
QArgs :: query_args(), QArgs :: query_args(),
Headers :: headers(), Headers :: headers(),
Payload :: string()) -> string(). Payload :: string()
) -> string().
%% @doc Create the request hash value %% @doc Create the request hash value
%% @end %% @end
request_hash(Method, Path, QArgs, Headers, Payload) -> request_hash(Method, Path, QArgs, Headers, Payload) ->
RawPath = case string:slice(Path, 0, 1) of RawPath =
"/" -> Path; case string:slice(Path, 0, 1) of
_ -> "/" ++ Path "/" -> Path;
end, _ -> "/" ++ Path
EncodedPath = uri_string:recompose(#{path => RawPath}), end,
CanonicalRequest = string:join([string:to_upper(atom_to_list(Method)), EncodedPath = uri_string:recompose(#{path => RawPath}),
EncodedPath, CanonicalRequest = string:join(
query_string(QArgs), [
canonical_headers(Headers), string:to_upper(atom_to_list(Method)),
signed_headers(Headers), EncodedPath,
sha256(Payload)], "\n"), query_string(QArgs),
sha256(CanonicalRequest). canonical_headers(Headers),
signed_headers(Headers),
sha256(Payload)
],
"\n"
),
sha256(CanonicalRequest).
-spec scope(
-spec scope(AMZDate :: string(), AMZDate :: string(),
Region :: region(), Region :: region(),
Service :: string()) -> string(). Service :: string()
) -> string().
%% @doc Create the Scope string %% @doc Create the Scope string
%% @end %% @end
scope(AMZDate, Region, Service) -> scope(AMZDate, Region, Service) ->
string:join([AMZDate, Region, Service, "aws4_request"], "/"). string:join([AMZDate, Region, Service, "aws4_request"], "/").
-spec sha256(Value :: string()) -> string(). -spec sha256(Value :: string()) -> string().
%% @doc Return the SHA-256 hash for the specified value. %% @doc Return the SHA-256 hash for the specified value.
%% @end %% @end
sha256(Value) -> sha256(Value) ->
lists:flatten(io_lib:format("~64.16.0b", lists:flatten(
[binary:decode_unsigned(crypto:hash(sha256, Value))])). io_lib:format(
"~64.16.0b",
[binary:decode_unsigned(crypto:hash(sha256, Value))]
)
).
-spec signed_headers(Headers :: list()) -> string(). -spec signed_headers(Headers :: list()) -> string().
%% @doc Return the signed headers string of delimited header key names %% @doc Return the signed headers string of delimited header key names
%% @end %% @end
signed_headers(Headers) -> signed_headers(Headers) ->
signed_headers(sort_headers(Headers), []). signed_headers(sort_headers(Headers), []).
-spec signed_headers(Headers :: headers(), Values :: list()) -> string(). -spec signed_headers(Headers :: headers(), Values :: list()) -> string().
%% @doc Return the signed headers string of delimited header key names %% @doc Return the signed headers string of delimited header key names
%% @end %% @end
signed_headers([], SignedHeaders) -> string:join(SignedHeaders, ";"); signed_headers([], SignedHeaders) ->
signed_headers([{Key,_}|T], SignedHeaders) -> string:join(SignedHeaders, ";");
signed_headers(T, SignedHeaders ++ [string:to_lower(Key)]). signed_headers([{Key, _} | T], SignedHeaders) ->
signed_headers(T, SignedHeaders ++ [string:to_lower(Key)]).
-spec signature(
-spec signature(StringToSign :: string(), StringToSign :: string(),
SigningKey :: string()) -> string(). SigningKey :: string()
) -> string().
%% @doc Create the request signature. %% @doc Create the request signature.
%% @end %% @end
signature(StringToSign, SigningKey) -> signature(StringToSign, SigningKey) ->
SignedValue = crypto:mac(hmac, sha256, SigningKey, StringToSign), SignedValue = crypto:mac(hmac, sha256, SigningKey, StringToSign),
lists:flatten(io_lib:format("~64.16.0b", [binary:decode_unsigned(SignedValue)])). lists:flatten(io_lib:format("~64.16.0b", [binary:decode_unsigned(SignedValue)])).
-spec signing_key(
-spec signing_key(SecretKey :: secret_access_key(), SecretKey :: secret_access_key(),
AMZDate :: string(), AMZDate :: string(),
Region :: region(), Region :: region(),
Service :: string()) -> string(). Service :: string()
) -> string().
%% @doc Create the signing key %% @doc Create the signing key
%% @end %% @end
signing_key(SecretKey, AMZDate, Region, Service) -> signing_key(SecretKey, AMZDate, Region, Service) ->
DateKey = hmac_sign("AWS4" ++ SecretKey, AMZDate), DateKey = hmac_sign("AWS4" ++ SecretKey, AMZDate),
RegionKey = hmac_sign(DateKey, Region), RegionKey = hmac_sign(DateKey, Region),
ServiceKey = hmac_sign(RegionKey, Service), ServiceKey = hmac_sign(RegionKey, Service),
hmac_sign(ServiceKey, "aws4_request"). hmac_sign(ServiceKey, "aws4_request").
-spec string_to_sign(
-spec string_to_sign(RequestTimestamp :: string(), RequestTimestamp :: string(),
RequestDate :: string(), RequestDate :: string(),
Region :: region(), Region :: region(),
Service :: string(), Service :: string(),
RequestHash :: string()) -> string(). RequestHash :: string()
) -> string().
%% @doc Return the string to sign when creating the signed request. %% @doc Return the string to sign when creating the signed request.
%% @end %% @end
string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash) -> string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash) ->
CredentialScope = credential_scope(RequestDate, Region, Service), CredentialScope = credential_scope(RequestDate, Region, Service),
lists:flatten(string:join([ lists:flatten(
?ALGORITHM, string:join(
RequestTimestamp, [
CredentialScope, ?ALGORITHM,
RequestHash RequestTimestamp,
], "\n")). CredentialScope,
RequestHash
],
"\n"
)
).
-spec sort_headers(Headers :: headers()) -> headers(). -spec sort_headers(Headers :: headers()) -> headers().
%% @doc Case-insensitive sorting of the request headers %% @doc Case-insensitive sorting of the request headers
%% @end %% @end
sort_headers(Headers) -> sort_headers(Headers) ->
lists:sort(fun({A,_}, {B, _}) -> string:to_lower(A) =< string:to_lower(B) end, Headers). lists:sort(fun({A, _}, {B, _}) -> string:to_lower(A) =< string:to_lower(B) end, Headers).

View File

@ -8,13 +8,15 @@
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0, -export([
init/1]). start_link/0,
init/1
]).
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5, Type, [I]}). -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5, Type, [I]}).
start_link() -> start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) -> init([]) ->
{ok, {{one_for_one, 5, 10}, [?CHILD(rabbitmq_aws, worker)]}}. {ok, {{one_for_one, 5, 10}, [?CHILD(rabbitmq_aws, worker)]}}.

View File

@ -7,12 +7,13 @@
%% ==================================================================== %% ====================================================================
-module(rabbitmq_aws_urilib). -module(rabbitmq_aws_urilib).
-export([build/1, -export([
build_query_string/1, build/1,
parse/1, build_query_string/1,
parse_userinfo/1, parse/1,
parse_userinfo_result/1 parse_userinfo/1,
]). parse_userinfo_result/1
]).
%% Export all for unit tests %% Export all for unit tests
-ifdef(TEST). -ifdef(TEST).
@ -25,77 +26,83 @@
%% @doc Build a URI string %% @doc Build a URI string
%% @end %% @end
build(URI) -> build(URI) ->
{UserInfo, Host, Port} = URI#uri.authority, {UserInfo, Host, Port} = URI#uri.authority,
UriMap = #{ UriMap = #{
scheme => to_list(URI#uri.scheme), scheme => to_list(URI#uri.scheme),
host => Host host => Host
}, },
UriMap1 = case UserInfo of UriMap1 =
undefined -> UriMap; case UserInfo of
{User, undefined} -> maps:put(userinfo, User, UriMap); undefined -> UriMap;
{User, Password} -> maps:put(userinfo, User ++ ":" ++ Password, UriMap) {User, undefined} -> maps:put(userinfo, User, UriMap);
end, {User, Password} -> maps:put(userinfo, User ++ ":" ++ Password, UriMap)
UriMap2 = case Port of end,
undefined -> UriMap1; UriMap2 =
Value1 -> maps:put(port, Value1, UriMap1) case Port of
end, undefined -> UriMap1;
UriMap3 = case URI#uri.path of Value1 -> maps:put(port, Value1, UriMap1)
undefined -> maps:put(path, "", UriMap2); end,
Value2 -> UriMap3 =
PrefixedPath = case string:slice(Value2, 0, 1) of case URI#uri.path of
"/" -> Value2; undefined ->
_ -> "/" ++ Value2 maps:put(path, "", UriMap2);
end, Value2 ->
maps:put(path, PrefixedPath, UriMap2) PrefixedPath =
end, case string:slice(Value2, 0, 1) of
UriMap4 = case URI#uri.query of "/" -> Value2;
undefined -> UriMap3; _ -> "/" ++ Value2
"" -> UriMap3; end,
Value3 -> maps:put(query, build_query_string(Value3), UriMap3) maps:put(path, PrefixedPath, UriMap2)
end, end,
UriMap5 = case URI#uri.fragment of UriMap4 =
undefined -> UriMap4; case URI#uri.query of
Value4 -> maps:put(fragment, Value4, UriMap4) undefined -> UriMap3;
end, "" -> UriMap3;
uri_string:recompose(UriMap5). Value3 -> maps:put(query, build_query_string(Value3), UriMap3)
end,
UriMap5 =
case URI#uri.fragment of
undefined -> UriMap4;
Value4 -> maps:put(fragment, Value4, UriMap4)
end,
uri_string:recompose(UriMap5).
-spec parse(string()) -> #uri{} | {error, any()}. -spec parse(string()) -> #uri{} | {error, any()}.
%% @doc Parse a URI string returning a record with the parsed results %% @doc Parse a URI string returning a record with the parsed results
%% @end %% @end
parse(Value) -> parse(Value) ->
UriMap = uri_string:parse(Value), UriMap = uri_string:parse(Value),
Scheme = maps:get(scheme, UriMap, "https"), Scheme = maps:get(scheme, UriMap, "https"),
Host = maps:get(host, UriMap), Host = maps:get(host, UriMap),
DefaultPort = case Scheme of DefaultPort =
"http" -> 80; case Scheme of
"https" -> 443; "http" -> 80;
_ -> undefined "https" -> 443;
end, _ -> undefined
Port = maps:get(port, UriMap, DefaultPort), end,
UserInfo = parse_userinfo(maps:get(userinfo, UriMap, undefined)), Port = maps:get(port, UriMap, DefaultPort),
Path = maps:get(path, UriMap), UserInfo = parse_userinfo(maps:get(userinfo, UriMap, undefined)),
Query = maps:get(query, UriMap, ""), Path = maps:get(path, UriMap),
#uri{scheme = Scheme, Query = maps:get(query, UriMap, ""),
authority = {parse_userinfo(UserInfo), Host, Port}, #uri{
path = Path, scheme = Scheme,
query = uri_string:dissect_query(Query), authority = {parse_userinfo(UserInfo), Host, Port},
fragment = maps:get(fragment, UriMap, undefined) path = Path,
}. query = uri_string:dissect_query(Query),
fragment = maps:get(fragment, UriMap, undefined)
}.
-spec parse_userinfo(string() | undefined) ->
-spec parse_userinfo(string() | undefined) {username() | undefined, password() | undefined} | undefined.
-> {username() | undefined, password() | undefined} | undefined.
parse_userinfo(undefined) -> undefined; parse_userinfo(undefined) -> undefined;
parse_userinfo([]) -> undefined; parse_userinfo([]) -> undefined;
parse_userinfo({User, undefined}) -> {User, undefined}; parse_userinfo({User, undefined}) -> {User, undefined};
parse_userinfo({User, Password}) -> {User, Password}; parse_userinfo({User, Password}) -> {User, Password};
parse_userinfo(Value) -> parse_userinfo(Value) -> parse_userinfo_result(string:tokens(Value, ":")).
parse_userinfo_result(string:tokens(Value, ":")).
-spec parse_userinfo_result(list()) ->
-spec parse_userinfo_result(list()) {username() | undefined, password() | undefined} | undefined.
-> {username() | undefined, password() | undefined} | undefined.
parse_userinfo_result([User, Password]) -> {User, Password}; parse_userinfo_result([User, Password]) -> {User, Password};
parse_userinfo_result([User]) -> {User, undefined}; parse_userinfo_result([User]) -> {User, undefined};
parse_userinfo_result({User, undefined}) -> {User, undefined}; parse_userinfo_result({User, undefined}) -> {User, undefined};
@ -110,12 +117,12 @@ parse_userinfo_result(User) -> {User, undefined}.
-spec build_query_string([{any(), any()}]) -> string(). -spec build_query_string([{any(), any()}]) -> string().
build_query_string(Args) when is_list(Args) -> build_query_string(Args) when is_list(Args) ->
Normalized = [{to_list(K), to_list(V)} || {K, V} <- Args], Normalized = [{to_list(K), to_list(V)} || {K, V} <- Args],
uri_string:compose_query(Normalized). uri_string:compose_query(Normalized).
-spec to_list(Val :: integer() | list() | binary() | atom() | map()) -> list(). -spec to_list(Val :: integer() | list() | binary() | atom() | map()) -> list().
to_list(Val) when is_list(Val) -> Val; to_list(Val) when is_list(Val) -> Val;
to_list(Val) when is_map(Val) -> maps:to_list(Val); to_list(Val) when is_map(Val) -> maps:to_list(Val);
to_list(Val) when is_atom(Val) -> atom_to_list(Val); to_list(Val) when is_atom(Val) -> atom_to_list(Val);
to_list(Val) when is_binary(Val) -> binary_to_list(Val); to_list(Val) when is_binary(Val) -> binary_to_list(Val);
to_list(Val) when is_integer(Val) -> integer_to_list(Val). to_list(Val) when is_integer(Val) -> integer_to_list(Val).

View File

@ -12,35 +12,32 @@
-spec parse(Value :: string() | binary()) -> list(). -spec parse(Value :: string() | binary()) -> list().
parse(Value) -> parse(Value) ->
{Element, _} = xmerl_scan:string(Value), {Element, _} = xmerl_scan:string(Value),
parse_node(Element). parse_node(Element).
parse_node(#xmlElement{name = Name, content = Content}) ->
Value = parse_content(Content, []),
[{atom_to_list(Name), flatten_value(Value, Value)}].
parse_node(#xmlElement{name=Name, content=Content}) -> flatten_text([], Value) ->
Value = parse_content(Content, []), Value;
[{atom_to_list(Name), flatten_value(Value, Value)}]. flatten_text([{K, V} | T], Accum) when is_list(V) ->
flatten_text([], Value) -> Value;
flatten_text([{K,V}|T], Accum) when is_list(V) ->
flatten_text(T, lists:append([{K, V}], Accum)); flatten_text(T, lists:append([{K, V}], Accum));
flatten_text([H | T], Accum) when is_list(H) -> flatten_text([H | T], Accum) when is_list(H) ->
flatten_text(T, lists:append(T, Accum)). flatten_text(T, lists:append(T, Accum)).
flatten_value([L], _) when is_list(L) -> L; flatten_value([L], _) when is_list(L) -> L;
flatten_value(L, _) when is_list(L) -> flatten_text(L, []). flatten_value(L, _) when is_list(L) -> flatten_text(L, []).
parse_content([], Value) ->
parse_content([], Value) -> Value; Value;
parse_content(#xmlElement{} = Element, Accum) -> parse_content(#xmlElement{} = Element, Accum) ->
lists:append(parse_node(Element), Accum); lists:append(parse_node(Element), Accum);
parse_content(#xmlText{value=Value}, Accum) -> parse_content(#xmlText{value = Value}, Accum) ->
case string:strip(Value) of case string:strip(Value) of
"" -> Accum; "" -> Accum;
"\n" -> Accum; "\n" -> Accum;
Stripped -> Stripped -> lists:append([Stripped], Accum)
lists:append([Stripped], Accum) end;
end; parse_content([H | T], Accum) ->
parse_content([H|T], Accum) -> parse_content(T, parse_content(H, Accum)).
parse_content(T, parse_content(H, Accum)).

View File

@ -5,14 +5,14 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
run() -> run() ->
Result = { Result = {
eunit:test(rabbitmq_aws_app_tests, [verbose]), eunit:test(rabbitmq_aws_app_tests, [verbose]),
eunit:test(rabbitmq_aws_config_tests, [verbose]), eunit:test(rabbitmq_aws_config_tests, [verbose]),
eunit:test(rabbitmq_aws_json_tests, [verbose]), eunit:test(rabbitmq_aws_json_tests, [verbose]),
eunit:test(rabbitmq_aws_sign_tests, [verbose]), eunit:test(rabbitmq_aws_sign_tests, [verbose]),
eunit:test(rabbitmq_aws_sup_tests, [verbose]), eunit:test(rabbitmq_aws_sup_tests, [verbose]),
eunit:test(rabbitmq_aws_tests, [verbose]), eunit:test(rabbitmq_aws_tests, [verbose]),
eunit:test(rabbitmq_aws_urilib_tests, [verbose]), eunit:test(rabbitmq_aws_urilib_tests, [verbose]),
eunit:test(rabbitmq_aws_xml_tests, [verbose]) eunit:test(rabbitmq_aws_xml_tests, [verbose])
}, },
?assertEqual({ok, ok, ok, ok, ok, ok, ok, ok}, Result). ?assertEqual({ok, ok, ok, ok, ok, ok, ok, ok}, Result).

View File

@ -3,22 +3,23 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
start_test_() -> start_test_() ->
{foreach, {foreach,
fun() -> fun() ->
meck:new(rabbitmq_aws_sup, [passthrough]) meck:new(rabbitmq_aws_sup, [passthrough])
end, end,
fun(_) -> fun(_) ->
meck:unload(rabbitmq_aws_sup) meck:unload(rabbitmq_aws_sup)
end, end,
[ [
{"supervisor initialized", fun() -> {"supervisor initialized", fun() ->
meck:expect(rabbitmq_aws_sup, start_link, fun() -> {ok, test_result} end), meck:expect(rabbitmq_aws_sup, start_link, fun() -> {ok, test_result} end),
?assertEqual({ok, test_result}, ?assertEqual(
rabbitmq_aws_app:start(temporary, [])), {ok, test_result},
meck:validate(rabbitmq_aws_sup) rabbitmq_aws_app:start(temporary, [])
end} ),
] meck:validate(rabbitmq_aws_sup)
}. end}
]}.
stop_test() -> stop_test() ->
?assertEqual(ok, rabbitmq_aws_app:stop({})). ?assertEqual(ok, rabbitmq_aws_app:stop({})).

View File

@ -4,442 +4,535 @@
-include("rabbitmq_aws.hrl"). -include("rabbitmq_aws.hrl").
config_file_test_() -> config_file_test_() ->
[ [
{"from environment variable", fun() -> {"from environment variable", fun() ->
os:putenv("AWS_CONFIG_FILE", "/etc/aws/config"), os:putenv("AWS_CONFIG_FILE", "/etc/aws/config"),
?assertEqual("/etc/aws/config", rabbitmq_aws_config:config_file()) ?assertEqual("/etc/aws/config", rabbitmq_aws_config:config_file())
end}, end},
{"default without environment variable", fun() -> {"default without environment variable", fun() ->
os:unsetenv("AWS_CONFIG_FILE"), os:unsetenv("AWS_CONFIG_FILE"),
os:putenv("HOME", "/home/rrabbit"), os:putenv("HOME", "/home/rrabbit"),
?assertEqual("/home/rrabbit/.aws/config", ?assertEqual(
rabbitmq_aws_config:config_file()) "/home/rrabbit/.aws/config",
end} rabbitmq_aws_config:config_file()
]. )
end}
].
config_file_data_test_() -> config_file_data_test_() ->
[ [
{"successfully parses ini", fun() -> {"successfully parses ini", fun() ->
setup_test_config_env_var(), setup_test_config_env_var(),
Expectation = [ Expectation = [
{"default", {"default", [
[{aws_access_key_id, "default-key"}, {aws_access_key_id, "default-key"},
{aws_secret_access_key, "default-access-key"}, {aws_secret_access_key, "default-access-key"},
{region, "us-east-4"}]}, {region, "us-east-4"}
{"profile testing", ]},
[{aws_access_key_id, "foo1"}, {"profile testing", [
{aws_secret_access_key, "bar2"}, {aws_access_key_id, "foo1"},
{s3, [{max_concurrent_requests, 10}, {aws_secret_access_key, "bar2"},
{max_queue_size, 1000}]}, {s3, [
{region, "us-west-5"}]}, {max_concurrent_requests, 10},
{"profile no-region", {max_queue_size, 1000}
[{aws_access_key_id, "foo2"}, ]},
{aws_secret_access_key, "bar3"}]}, {region, "us-west-5"}
{"profile only-key", ]},
[{aws_access_key_id, "foo3"}]}, {"profile no-region", [
{"profile only-secret", {aws_access_key_id, "foo2"},
[{aws_secret_access_key, "foo4"}]}, {aws_secret_access_key, "bar3"}
{"profile bad-entry", ]},
[{aws_secret_access, "foo5"}]} {"profile only-key", [{aws_access_key_id, "foo3"}]},
], {"profile only-secret", [{aws_secret_access_key, "foo4"}]},
?assertEqual(Expectation, {"profile bad-entry", [{aws_secret_access, "foo5"}]}
rabbitmq_aws_config:config_file_data()) ],
end}, ?assertEqual(
{"file does not exist", fun() -> Expectation,
?assertEqual({error, enoent}, rabbitmq_aws_config:config_file_data()
rabbitmq_aws_config:ini_file_data(filename:join([filename:absname("."), "bad_path"]), false)) )
end end},
}, {"file does not exist", fun() ->
{"file exists but path is invalid", fun() -> ?assertEqual(
?assertEqual({error, enoent}, {error, enoent},
rabbitmq_aws_config:ini_file_data(filename:join([filename:absname("."), "bad_path"]), true)) rabbitmq_aws_config:ini_file_data(
end filename:join([filename:absname("."), "bad_path"]), false
} )
]. )
end},
{"file exists but path is invalid", fun() ->
?assertEqual(
{error, enoent},
rabbitmq_aws_config:ini_file_data(
filename:join([filename:absname("."), "bad_path"]), true
)
)
end}
].
instance_metadata_test_() -> instance_metadata_test_() ->
[ [
{"instance role URL", fun() -> {"instance role URL", fun() ->
?assertEqual("http://169.254.169.254/latest/meta-data/iam/security-credentials", ?assertEqual(
rabbitmq_aws_config:instance_role_url()) "http://169.254.169.254/latest/meta-data/iam/security-credentials",
end}, rabbitmq_aws_config:instance_role_url()
{"availability zone URL", fun() -> )
?assertEqual("http://169.254.169.254/latest/meta-data/placement/availability-zone", end},
rabbitmq_aws_config:instance_availability_zone_url()) {"availability zone URL", fun() ->
end}, ?assertEqual(
{"instance id URL", fun() -> "http://169.254.169.254/latest/meta-data/placement/availability-zone",
?assertEqual("http://169.254.169.254/latest/meta-data/instance-id", rabbitmq_aws_config:instance_availability_zone_url()
rabbitmq_aws_config:instance_id_url()) )
end}, end},
{"arbitrary paths", fun () -> {"instance id URL", fun() ->
?assertEqual("http://169.254.169.254/a/b/c", rabbitmq_aws_config:instance_metadata_url("a/b/c")), ?assertEqual(
?assertEqual("http://169.254.169.254/a/b/c", rabbitmq_aws_config:instance_metadata_url("/a/b/c")) "http://169.254.169.254/latest/meta-data/instance-id",
end} rabbitmq_aws_config:instance_id_url()
]. )
end},
{"arbitrary paths", fun() ->
?assertEqual(
"http://169.254.169.254/a/b/c", rabbitmq_aws_config:instance_metadata_url("a/b/c")
),
?assertEqual(
"http://169.254.169.254/a/b/c", rabbitmq_aws_config:instance_metadata_url("/a/b/c")
)
end}
].
credentials_file_test_() -> credentials_file_test_() ->
[ [
{"from environment variable", fun() -> {"from environment variable", fun() ->
os:putenv("AWS_SHARED_CREDENTIALS_FILE", "/etc/aws/credentials"), os:putenv("AWS_SHARED_CREDENTIALS_FILE", "/etc/aws/credentials"),
?assertEqual("/etc/aws/credentials", rabbitmq_aws_config:credentials_file()) ?assertEqual("/etc/aws/credentials", rabbitmq_aws_config:credentials_file())
end}, end},
{"default without environment variable", fun() -> {"default without environment variable", fun() ->
os:unsetenv("AWS_SHARED_CREDENTIALS_FILE"), os:unsetenv("AWS_SHARED_CREDENTIALS_FILE"),
os:putenv("HOME", "/home/rrabbit"), os:putenv("HOME", "/home/rrabbit"),
?assertEqual("/home/rrabbit/.aws/credentials", ?assertEqual(
rabbitmq_aws_config:credentials_file()) "/home/rrabbit/.aws/credentials",
end} rabbitmq_aws_config:credentials_file()
]. )
end}
].
credentials_test_() -> credentials_test_() ->
{ {
foreach, foreach,
fun () -> fun() ->
meck:new(httpc), meck:new(httpc),
meck:new(rabbitmq_aws), meck:new(rabbitmq_aws),
reset_environment(), reset_environment(),
[httpc, rabbitmq_aws] [httpc, rabbitmq_aws]
end, end,
fun meck:unload/1, fun meck:unload/1,
[ [
{"from environment variables", fun() -> {"from environment variables", fun() ->
os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), os:putenv("AWS_ACCESS_KEY_ID", "Sésame"),
os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"),
?assertEqual({ok, "Sésame", "ouvre-toi", undefined, undefined}, ?assertEqual(
rabbitmq_aws_config:credentials()) {ok, "Sésame", "ouvre-toi", undefined, undefined},
end}, rabbitmq_aws_config:credentials()
{"from config file with default profile", fun() -> )
setup_test_config_env_var(), end},
?assertEqual({ok, "default-key", "default-access-key", undefined, undefined}, {"from config file with default profile", fun() ->
rabbitmq_aws_config:credentials()) setup_test_config_env_var(),
end}, ?assertEqual(
{"with missing environment variable", fun() -> {ok, "default-key", "default-access-key", undefined, undefined},
os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), rabbitmq_aws_config:credentials()
meck:sequence(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "secret_imdsv2_token"), )
?assertEqual({error, undefined}, end},
rabbitmq_aws_config:credentials()) {"with missing environment variable", fun() ->
end}, os:putenv("AWS_ACCESS_KEY_ID", "Sésame"),
{"from config file with default profile", fun() -> meck:sequence(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "secret_imdsv2_token"),
setup_test_config_env_var(), ?assertEqual(
?assertEqual({ok, "default-key", "default-access-key", undefined, undefined}, {error, undefined},
rabbitmq_aws_config:credentials()) rabbitmq_aws_config:credentials()
end}, )
{"from config file with profile", fun() -> end},
setup_test_config_env_var(), {"from config file with default profile", fun() ->
?assertEqual({ok, "foo1", "bar2", undefined, undefined}, setup_test_config_env_var(),
rabbitmq_aws_config:credentials("testing")) ?assertEqual(
end}, {ok, "default-key", "default-access-key", undefined, undefined},
{"from config file with bad profile", fun() -> rabbitmq_aws_config:credentials()
setup_test_config_env_var(), )
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), end},
?assertEqual({error, undefined}, {"from config file with profile", fun() ->
rabbitmq_aws_config:credentials("bad-profile-name")) setup_test_config_env_var(),
end}, ?assertEqual(
{"from credentials file with default profile", fun() -> {ok, "foo1", "bar2", undefined, undefined},
setup_test_credentials_env_var(), rabbitmq_aws_config:credentials("testing")
)
?assertEqual({ok, "foo1", "bar1", undefined, undefined}, end},
rabbitmq_aws_config:credentials()) {"from config file with bad profile", fun() ->
end}, setup_test_config_env_var(),
{"from credentials file with profile", fun() -> meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
setup_test_credentials_env_var(), ?assertEqual(
?assertEqual({ok, "foo2", "bar2", undefined, undefined}, {error, undefined},
rabbitmq_aws_config:credentials("development")) rabbitmq_aws_config:credentials("bad-profile-name")
end}, )
{"from credentials file with bad profile", fun() -> end},
setup_test_credentials_env_var(), {"from credentials file with default profile", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), setup_test_credentials_env_var(),
?assertEqual({error, undefined},
rabbitmq_aws_config:credentials("bad-profile-name"))
end},
{"from credentials file with only the key in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual({error, undefined},
rabbitmq_aws_config:credentials("only-key"))
end},
{"from credentials file with only the value in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual({error, undefined},
rabbitmq_aws_config:credentials("only-value"))
end},
{"from credentials file with missing keys in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual({error, undefined},
rabbitmq_aws_config:credentials("bad-entry"))
end},
{"from instance metadata service", fun() ->
CredsBody = "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2016-03-31T21:51:49Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIAIMAFAKEACCESSKEY\",\n \"SecretAccessKey\" : \"2+t64tZZVaz0yp0x1G23ZRYn+FAKEyVALUEs/4qh\",\n \"Token\" : \"FAKE//////////wEAK/TOKEN/VALUE=\",\n \"Expiration\" : \"2016-04-01T04:13:28Z\"\n}",
meck:sequence(httpc, request, 4,
[{ok, {{protocol, 200, message}, headers, "Bob"}},
{ok, {{protocol, 200, message}, headers, CredsBody}}]),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
Expectation = {ok, "ASIAIMAFAKEACCESSKEY", "2+t64tZZVaz0yp0x1G23ZRYn+FAKEyVALUEs/4qh",
{{2016,4,1},{4,13,28}}, "FAKE//////////wEAK/TOKEN/VALUE="},
?assertEqual(Expectation, rabbitmq_aws_config:credentials())
end
},
{"with instance metadata service role error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(httpc, request, 4, {error, timeout}),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end
},
{"with instance metadata service role http error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(httpc, request, 4,
{ok, {{protocol, 500, message}, headers, "Internal Server Error"}}),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end
},
{"with instance metadata service credentials error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:sequence(httpc, request, 4,
[{ok, {{protocol, 200, message}, headers, "Bob"}},
{error, timeout}]),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end
},
{"with instance metadata service credentials not found", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:sequence(httpc, request, 4,
[{ok, {{protocol, 200, message}, headers, "Bob"}},
{ok, {{protocol, 404, message}, headers, "File Not Found"}}]),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end
}
]}.
?assertEqual(
{ok, "foo1", "bar1", undefined, undefined},
rabbitmq_aws_config:credentials()
)
end},
{"from credentials file with profile", fun() ->
setup_test_credentials_env_var(),
?assertEqual(
{ok, "foo2", "bar2", undefined, undefined},
rabbitmq_aws_config:credentials("development")
)
end},
{"from credentials file with bad profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual(
{error, undefined},
rabbitmq_aws_config:credentials("bad-profile-name")
)
end},
{"from credentials file with only the key in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual(
{error, undefined},
rabbitmq_aws_config:credentials("only-key")
)
end},
{"from credentials file with only the value in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual(
{error, undefined},
rabbitmq_aws_config:credentials("only-value")
)
end},
{"from credentials file with missing keys in profile", fun() ->
setup_test_credentials_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual(
{error, undefined},
rabbitmq_aws_config:credentials("bad-entry")
)
end},
{"from instance metadata service", fun() ->
CredsBody =
"{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2016-03-31T21:51:49Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIAIMAFAKEACCESSKEY\",\n \"SecretAccessKey\" : \"2+t64tZZVaz0yp0x1G23ZRYn+FAKEyVALUEs/4qh\",\n \"Token\" : \"FAKE//////////wEAK/TOKEN/VALUE=\",\n \"Expiration\" : \"2016-04-01T04:13:28Z\"\n}",
meck:sequence(
httpc,
request,
4,
[
{ok, {{protocol, 200, message}, headers, "Bob"}},
{ok, {{protocol, 200, message}, headers, CredsBody}}
]
),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
Expectation =
{ok, "ASIAIMAFAKEACCESSKEY", "2+t64tZZVaz0yp0x1G23ZRYn+FAKEyVALUEs/4qh",
{{2016, 4, 1}, {4, 13, 28}}, "FAKE//////////wEAK/TOKEN/VALUE="},
?assertEqual(Expectation, rabbitmq_aws_config:credentials())
end},
{"with instance metadata service role error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(httpc, request, 4, {error, timeout}),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end},
{"with instance metadata service role http error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(
httpc,
request,
4,
{ok, {{protocol, 500, message}, headers, "Internal Server Error"}}
),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end},
{"with instance metadata service credentials error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:sequence(
httpc,
request,
4,
[
{ok, {{protocol, 200, message}, headers, "Bob"}},
{error, timeout}
]
),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end},
{"with instance metadata service credentials not found", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:sequence(
httpc,
request,
4,
[
{ok, {{protocol, 200, message}, headers, "Bob"}},
{ok, {{protocol, 404, message}, headers, "File Not Found"}}
]
),
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
end}
]
}.
home_path_test_() -> home_path_test_() ->
[ [
{"with HOME", fun() -> {"with HOME", fun() ->
os:putenv("HOME", "/home/rrabbit"), os:putenv("HOME", "/home/rrabbit"),
?assertEqual("/home/rrabbit", ?assertEqual(
rabbitmq_aws_config:home_path()) "/home/rrabbit",
end}, rabbitmq_aws_config:home_path()
{"without HOME", fun() -> )
os:unsetenv("HOME"), end},
?assertEqual(filename:absname("."), {"without HOME", fun() ->
rabbitmq_aws_config:home_path()) os:unsetenv("HOME"),
end} ?assertEqual(
]. filename:absname("."),
rabbitmq_aws_config:home_path()
)
end}
].
ini_format_key_test_() -> ini_format_key_test_() ->
[ [
{"when value is list", fun() -> {"when value is list", fun() ->
?assertEqual(test_key, rabbitmq_aws_config:ini_format_key("test_key")) ?assertEqual(test_key, rabbitmq_aws_config:ini_format_key("test_key"))
end}, end},
{"when value is binary", fun() -> {"when value is binary", fun() ->
?assertEqual({error, type}, rabbitmq_aws_config:ini_format_key(<<"test_key">>)) ?assertEqual({error, type}, rabbitmq_aws_config:ini_format_key(<<"test_key">>))
end} end}
]. ].
maybe_convert_number_test_() -> maybe_convert_number_test_() ->
[ [
{"when string contains an integer", fun() -> {"when string contains an integer", fun() ->
?assertEqual(123, rabbitmq_aws_config:maybe_convert_number("123")) ?assertEqual(123, rabbitmq_aws_config:maybe_convert_number("123"))
end}, end},
{"when string contains a float", fun() -> {"when string contains a float", fun() ->
?assertEqual(123.456, rabbitmq_aws_config:maybe_convert_number("123.456")) ?assertEqual(123.456, rabbitmq_aws_config:maybe_convert_number("123.456"))
end}, end},
{"when string does not contain a number", fun() -> {"when string does not contain a number", fun() ->
?assertEqual("hello, world", rabbitmq_aws_config:maybe_convert_number("hello, world")) ?assertEqual("hello, world", rabbitmq_aws_config:maybe_convert_number("hello, world"))
end} end}
]. ].
parse_iso8601_test_() -> parse_iso8601_test_() ->
[ [
{"parse test", fun() -> {"parse test", fun() ->
Value = "2016-05-19T18:25:23Z", Value = "2016-05-19T18:25:23Z",
Expectation = {{2016,5,19},{18,25,23}}, Expectation = {{2016, 5, 19}, {18, 25, 23}},
?assertEqual(Expectation, rabbitmq_aws_config:parse_iso8601_timestamp(Value)) ?assertEqual(Expectation, rabbitmq_aws_config:parse_iso8601_timestamp(Value))
end} end}
]. ].
profile_test_() -> profile_test_() ->
[ [
{"from environment variable", fun() -> {"from environment variable", fun() ->
os:putenv("AWS_DEFAULT_PROFILE", "httpc-aws test"), os:putenv("AWS_DEFAULT_PROFILE", "httpc-aws test"),
?assertEqual("httpc-aws test", rabbitmq_aws_config:profile()) ?assertEqual("httpc-aws test", rabbitmq_aws_config:profile())
end}, end},
{"default without environment variable", fun() -> {"default without environment variable", fun() ->
os:unsetenv("AWS_DEFAULT_PROFILE"), os:unsetenv("AWS_DEFAULT_PROFILE"),
?assertEqual("default", rabbitmq_aws_config:profile()) ?assertEqual("default", rabbitmq_aws_config:profile())
end} end}
]. ].
read_file_test_() -> read_file_test_() ->
[ [
{"file does not exist", fun() -> {"file does not exist", fun() ->
?assertEqual({error, enoent}, rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"]))) ?assertEqual(
end} {error, enoent},
]. rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"]))
)
end}
].
region_test_() -> region_test_() ->
{ {
foreach, foreach,
fun () -> fun() ->
meck:new(httpc), meck:new(httpc),
meck:new(rabbitmq_aws), meck:new(rabbitmq_aws),
reset_environment(), reset_environment(),
[httpc, rabbitmq_aws] [httpc, rabbitmq_aws]
end, end,
fun meck:unload/1, fun meck:unload/1,
[ [
{"with environment variable", fun() -> {"with environment variable", fun() ->
os:putenv("AWS_DEFAULT_REGION", "us-west-1"), os:putenv("AWS_DEFAULT_REGION", "us-west-1"),
?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region())
end}, end},
{"with config file and specified profile", fun() -> {"with config file and specified profile", fun() ->
setup_test_config_env_var(), setup_test_config_env_var(),
?assertEqual({ok, "us-west-5"}, rabbitmq_aws_config:region("testing")) ?assertEqual({ok, "us-west-5"}, rabbitmq_aws_config:region("testing"))
end}, end},
{"with config file using default profile", fun() -> {"with config file using default profile", fun() ->
setup_test_config_env_var(), setup_test_config_env_var(),
?assertEqual({ok, "us-east-4"}, rabbitmq_aws_config:region()) ?assertEqual({ok, "us-east-4"}, rabbitmq_aws_config:region())
end}, end},
{"missing profile in config", fun() -> {"missing profile in config", fun() ->
setup_test_config_env_var(), setup_test_config_env_var(),
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region("no-region")) ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region("no-region"))
end}, end},
{"from instance metadata service", fun() -> {"from instance metadata service", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(httpc, request, 4, meck:expect(
{ok, {{protocol, 200, message}, headers, "us-west-1a"}}), httpc,
?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) request,
end}, 4,
{"full lookup failure", fun() -> {ok, {{protocol, 200, message}, headers, "us-west-1a"}}
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), ),
?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region())
end}, end},
{"http error failure", fun() -> {"full lookup failure", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
meck:expect(httpc, request, 4, ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region())
{ok, {{protocol, 500, message}, headers, "Internal Server Error"}}), end},
?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) {"http error failure", fun() ->
end} meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
]}. meck:expect(
httpc,
request,
4,
{ok, {{protocol, 500, message}, headers, "Internal Server Error"}}
),
?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region())
end}
]
}.
instance_id_test_() -> instance_id_test_() ->
{ {
foreach, foreach,
fun () ->
meck:new(httpc),
meck:new(rabbitmq_aws),
reset_environment(),
[httpc, rabbitmq_aws]
end,
fun meck:unload/1,
[
{"get instance id successfully",
fun() -> fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), meck:new(httpc),
meck:expect(httpc, request, 4, {ok, {{protocol, 200, message}, headers, "instance-id"}}), meck:new(rabbitmq_aws),
?assertEqual({ok, "instance-id"}, rabbitmq_aws_config:instance_id()) reset_environment(),
end [httpc, rabbitmq_aws]
}, end,
{"getting instance id is rejected with invalid token error", fun meck:unload/1,
fun() -> [
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "invalid"), {"get instance id successfully", fun() ->
meck:expect(httpc, request, 4, {error, {{protocol, 401, message}, headers, "Invalid token"}}), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual({error, undefined}, rabbitmq_aws_config:instance_id()) meck:expect(
end httpc, request, 4, {ok, {{protocol, 200, message}, headers, "instance-id"}}
}, ),
{"getting instance id is rejected with access denied error", ?assertEqual({ok, "instance-id"}, rabbitmq_aws_config:instance_id())
fun() -> end},
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "expired token"), {"getting instance id is rejected with invalid token error", fun() ->
meck:expect(httpc, request, 4, {error, {{protocol, 403, message}, headers, "access denied"}}), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "invalid"),
?assertEqual({error, undefined}, rabbitmq_aws_config:instance_id()) meck:expect(
end httpc, request, 4, {error, {{protocol, 401, message}, headers, "Invalid token"}}
} ),
] ?assertEqual({error, undefined}, rabbitmq_aws_config:instance_id())
}. end},
{"getting instance id is rejected with access denied error", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "expired token"),
meck:expect(
httpc, request, 4, {error, {{protocol, 403, message}, headers, "access denied"}}
),
?assertEqual({error, undefined}, rabbitmq_aws_config:instance_id())
end}
]
}.
load_imdsv2_token_test_() -> load_imdsv2_token_test_() ->
{ {
foreach, foreach,
fun () ->
meck:new(httpc),
[httpc]
end,
fun meck:unload/1,
[
{"fail to get imdsv2 token - timeout",
fun() -> fun() ->
meck:expect(httpc, request, 4, {error, timeout}), meck:new(httpc),
?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token()) [httpc]
end}, end,
{"fail to get imdsv2 token - PUT request is not valid", fun meck:unload/1,
fun() -> [
meck:expect(httpc, request, 4, {error, {{protocol, 400, messge}, headers, "Missing or Invalid Parameters The PUT request is not valid."}}), {"fail to get imdsv2 token - timeout", fun() ->
?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token()) meck:expect(httpc, request, 4, {error, timeout}),
end}, ?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token())
{"successfully get imdsv2 token from instance metadata service", end},
fun() -> {"fail to get imdsv2 token - PUT request is not valid", fun() ->
IMDSv2Token = "super_secret_token_value", meck:expect(
meck:sequence(httpc, request, 4, httpc,
[{ok, {{protocol, 200, message}, headers, IMDSv2Token}}]), request,
?assertEqual(IMDSv2Token, rabbitmq_aws_config:load_imdsv2_token()) 4,
end} {error, {
] {protocol, 400, messge},
}. headers,
"Missing or Invalid Parameters The PUT request is not valid."
}}
),
?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token())
end},
{"successfully get imdsv2 token from instance metadata service", fun() ->
IMDSv2Token = "super_secret_token_value",
meck:sequence(
httpc,
request,
4,
[{ok, {{protocol, 200, message}, headers, IMDSv2Token}}]
),
?assertEqual(IMDSv2Token, rabbitmq_aws_config:load_imdsv2_token())
end}
]
}.
maybe_imdsv2_token_headers_test_() -> maybe_imdsv2_token_headers_test_() ->
{ {
foreach, foreach,
fun () -> fun() ->
meck:new(rabbitmq_aws), meck:new(rabbitmq_aws),
[rabbitmq_aws] [rabbitmq_aws]
end, end,
fun meck:unload/1, fun meck:unload/1,
[ [
{"imdsv2 token is not available", fun() -> {"imdsv2 token is not available", fun() ->
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined),
?assertEqual([], rabbitmq_aws_config:maybe_imdsv2_token_headers()) ?assertEqual([], rabbitmq_aws_config:maybe_imdsv2_token_headers())
end} end},
,
{"imdsv2 is available", fun() -> {"imdsv2 is available", fun() ->
IMDSv2Token = "super_secret_token_value ;)", IMDSv2Token = "super_secret_token_value ;)",
meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, IMDSv2Token), meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, IMDSv2Token),
?assertEqual([{"X-aws-ec2-metadata-token", IMDSv2Token}], rabbitmq_aws_config:maybe_imdsv2_token_headers()) ?assertEqual(
end} [{"X-aws-ec2-metadata-token", IMDSv2Token}],
] rabbitmq_aws_config:maybe_imdsv2_token_headers()
}. )
end}
]
}.
reset_environment() -> reset_environment() ->
os:unsetenv("AWS_ACCESS_KEY_ID"), os:unsetenv("AWS_ACCESS_KEY_ID"),
os:unsetenv("AWS_DEFAULT_REGION"), os:unsetenv("AWS_DEFAULT_REGION"),
os:unsetenv("AWS_SECRET_ACCESS_KEY"), os:unsetenv("AWS_SECRET_ACCESS_KEY"),
setup_test_file_with_env_var("AWS_CONFIG_FILE", "bad_config.ini"), setup_test_file_with_env_var("AWS_CONFIG_FILE", "bad_config.ini"),
setup_test_file_with_env_var("AWS_SHARED_CREDENTIALS_FILE", setup_test_file_with_env_var(
"bad_credentials.ini"), "AWS_SHARED_CREDENTIALS_FILE",
meck:expect(httpc, request, 4, {error, timeout}). "bad_credentials.ini"
),
meck:expect(httpc, request, 4, {error, timeout}).
setup_test_config_env_var() -> setup_test_config_env_var() ->
setup_test_file_with_env_var("AWS_CONFIG_FILE", "test_aws_config.ini"). setup_test_file_with_env_var("AWS_CONFIG_FILE", "test_aws_config.ini").
setup_test_file_with_env_var(EnvVar, Filename) -> setup_test_file_with_env_var(EnvVar, Filename) ->
os:putenv(EnvVar, os:putenv(
filename:join([filename:absname("."), "test", EnvVar,
Filename])). filename:join([
filename:absname("."),
"test",
Filename
])
).
setup_test_credentials_env_var() -> setup_test_credentials_env_var() ->
setup_test_file_with_env_var("AWS_SHARED_CREDENTIALS_FILE", setup_test_file_with_env_var(
"test_aws_credentials.ini"). "AWS_SHARED_CREDENTIALS_FILE",
"test_aws_credentials.ini"
).

View File

@ -3,69 +3,93 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
parse_test_() -> parse_test_() ->
[ [
{"string decoding", fun() -> {"string decoding", fun() ->
Value = "{\"requestId\":\"bda7fbdb-eddb-41fa-8626-7ba87923d690\",\"number\":128,\"enabled\":true,\"tagSet\":[{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"Environment\",\"value\":\"prod-us-east-1\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:logical-id\",\"value\":\"AutoScalingGroup\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:stack-name\",\"value\":\"prod-us-east-1-ecs-1\"}]}", Value =
Expectation = [ "{\"requestId\":\"bda7fbdb-eddb-41fa-8626-7ba87923d690\",\"number\":128,\"enabled\":true,\"tagSet\":[{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"Environment\",\"value\":\"prod-us-east-1\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:logical-id\",\"value\":\"AutoScalingGroup\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:stack-name\",\"value\":\"prod-us-east-1-ecs-1\"}]}",
{"requestId","bda7fbdb-eddb-41fa-8626-7ba87923d690"}, Expectation = [
{"number", 128}, {"requestId", "bda7fbdb-eddb-41fa-8626-7ba87923d690"},
{"enabled", true}, {"number", 128},
{"tagSet", {"enabled", true},
[{"resourceId","i-13a4abea"}, {"tagSet", [
{"resourceType","instance"}, {"resourceId", "i-13a4abea"},
{"key","Environment"}, {"resourceType", "instance"},
{"value","prod-us-east-1"}, {"key", "Environment"},
{"resourceId","i-13a4abea"}, {"value", "prod-us-east-1"},
{"resourceType","instance"}, {"resourceId", "i-13a4abea"},
{"key","aws:cloudformation:logical-id"}, {"resourceType", "instance"},
{"value","AutoScalingGroup"}, {"key", "aws:cloudformation:logical-id"},
{"resourceId","i-13a4abea"}, {"value", "AutoScalingGroup"},
{"resourceType","instance"}, {"resourceId", "i-13a4abea"},
{"key","aws:cloudformation:stack-name"}, {"resourceType", "instance"},
{"value","prod-us-east-1-ecs-1"}]} {"key", "aws:cloudformation:stack-name"},
], {"value", "prod-us-east-1-ecs-1"}
Proplist = rabbitmq_aws_json:decode(Value), ]}
?assertEqual(proplists:get_value("requestId", Expectation), proplists:get_value("requestId", Proplist)), ],
?assertEqual(proplists:get_value("number", Expectation), proplists:get_value("number", Proplist)), Proplist = rabbitmq_aws_json:decode(Value),
?assertEqual(proplists:get_value("enabled", Expectation), proplists:get_value("enabled", Proplist)), ?assertEqual(
?assertEqual(lists:usort(proplists:get_value("tagSet", Expectation)), proplists:get_value("requestId", Expectation),
lists:usort(proplists:get_value("tagSet", Proplist))) proplists:get_value("requestId", Proplist)
end}, ),
{"binary decoding", fun() -> ?assertEqual(
Value = <<"{\"requestId\":\"bda7fbdb-eddb-41fa-8626-7ba87923d690\",\"number\":128,\"enabled\":true,\"tagSet\":[{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"Environment\",\"value\":\"prod-us-east-1\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:logical-id\",\"value\":\"AutoScalingGroup\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:stack-name\",\"value\":\"prod-us-east-1-ecs-1\"}]}">>, proplists:get_value("number", Expectation), proplists:get_value("number", Proplist)
Expectation = [ ),
{"requestId","bda7fbdb-eddb-41fa-8626-7ba87923d690"}, ?assertEqual(
{"number", 128}, proplists:get_value("enabled", Expectation),
{"enabled", true}, proplists:get_value("enabled", Proplist)
{"tagSet", ),
[{"resourceId","i-13a4abea"}, ?assertEqual(
{"resourceType","instance"}, lists:usort(proplists:get_value("tagSet", Expectation)),
{"key","Environment"}, lists:usort(proplists:get_value("tagSet", Proplist))
{"value","prod-us-east-1"}, )
{"resourceId","i-13a4abea"}, end},
{"resourceType","instance"}, {"binary decoding", fun() ->
{"key","aws:cloudformation:logical-id"}, Value =
{"value","AutoScalingGroup"}, <<"{\"requestId\":\"bda7fbdb-eddb-41fa-8626-7ba87923d690\",\"number\":128,\"enabled\":true,\"tagSet\":[{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"Environment\",\"value\":\"prod-us-east-1\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:logical-id\",\"value\":\"AutoScalingGroup\"},{\"resourceId\":\"i-13a4abea\",\"resourceType\":\"instance\",\"key\":\"aws:cloudformation:stack-name\",\"value\":\"prod-us-east-1-ecs-1\"}]}">>,
{"resourceId","i-13a4abea"}, Expectation = [
{"resourceType","instance"}, {"requestId", "bda7fbdb-eddb-41fa-8626-7ba87923d690"},
{"key","aws:cloudformation:stack-name"}, {"number", 128},
{"value","prod-us-east-1-ecs-1"}]} {"enabled", true},
], {"tagSet", [
Proplist = rabbitmq_aws_json:decode(Value), {"resourceId", "i-13a4abea"},
?assertEqual(proplists:get_value("requestId", Expectation), proplists:get_value("requestId", Proplist)), {"resourceType", "instance"},
?assertEqual(proplists:get_value("number", Expectation), proplists:get_value("number", Proplist)), {"key", "Environment"},
?assertEqual(proplists:get_value("enabled", Expectation), proplists:get_value("enabled", Proplist)), {"value", "prod-us-east-1"},
?assertEqual(lists:usort(proplists:get_value("tagSet", Expectation)), {"resourceId", "i-13a4abea"},
lists:usort(proplists:get_value("tagSet", Proplist))) {"resourceType", "instance"},
end}, {"key", "aws:cloudformation:logical-id"},
{"list values", fun() -> {"value", "AutoScalingGroup"},
Value = "{\"misc\": [\"foo\", true, 123]\}", {"resourceId", "i-13a4abea"},
Expectation = [{"misc", ["foo", true, 123]}], {"resourceType", "instance"},
?assertEqual(Expectation, rabbitmq_aws_json:decode(Value)) {"key", "aws:cloudformation:stack-name"},
end}, {"value", "prod-us-east-1-ecs-1"}
{"empty objects", fun() -> ]}
Value = "{\"tags\": [{}]}", ],
Expectation = [{"tags", [{}]}], Proplist = rabbitmq_aws_json:decode(Value),
?assertEqual(Expectation, rabbitmq_aws_json:decode(Value)) ?assertEqual(
end} proplists:get_value("requestId", Expectation),
]. proplists:get_value("requestId", Proplist)
),
?assertEqual(
proplists:get_value("number", Expectation), proplists:get_value("number", Proplist)
),
?assertEqual(
proplists:get_value("enabled", Expectation),
proplists:get_value("enabled", Proplist)
),
?assertEqual(
lists:usort(proplists:get_value("tagSet", Expectation)),
lists:usort(proplists:get_value("tagSet", Proplist))
)
end},
{"list values", fun() ->
Value = "{\"misc\": [\"foo\", true, 123]\}",
Expectation = [{"misc", ["foo", true, 123]}],
?assertEqual(Expectation, rabbitmq_aws_json:decode(Value))
end},
{"empty objects", fun() ->
Value = "{\"tags\": [{}]}",
Expectation = [{"tags", [{}]}],
?assertEqual(Expectation, rabbitmq_aws_json:decode(Value))
end}
].

View File

@ -3,289 +3,457 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("rabbitmq_aws.hrl"). -include("rabbitmq_aws.hrl").
amz_date_test_() -> amz_date_test_() ->
[ [
{"value", fun() -> {"value", fun() ->
?assertEqual("20160220", ?assertEqual(
rabbitmq_aws_sign:amz_date("20160220T120000Z")) "20160220",
end} rabbitmq_aws_sign:amz_date("20160220T120000Z")
]. )
end}
].
append_headers_test_() -> append_headers_test_() ->
[ [
{"with security token", fun() -> {"with security token", fun() ->
Headers = [
{"Content-Type", "application/x-amz-json-1.0"},
{"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}
],
Headers = [{"Content-Type", "application/x-amz-json-1.0"}, AMZDate = "20160220T120000Z",
{"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}], ContentLength = 128,
PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213",
AMZDate = "20160220T120000Z", Hostname = "ec2.amazonaws.com",
ContentLength = 128, SecurityToken = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L",
PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213", Expectation = [
Hostname = "ec2.amazonaws.com", {"content-length", integer_to_list(ContentLength)},
SecurityToken = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", {"content-type", "application/x-amz-json-1.0"},
Expectation = [{"content-length", integer_to_list(ContentLength)}, {"date", AMZDate},
{"content-type", "application/x-amz-json-1.0"}, {"host", Hostname},
{"date", AMZDate}, {"x-amz-content-sha256", PayloadHash},
{"host", Hostname}, {"x-amz-security-token", SecurityToken},
{"x-amz-content-sha256", PayloadHash}, {"x-amz-target", "DynamoDB_20120810.DescribeTable"}
{"x-amz-security-token", SecurityToken}, ],
{"x-amz-target", "DynamoDB_20120810.DescribeTable"}], ?assertEqual(
?assertEqual(Expectation, Expectation,
rabbitmq_aws_sign:append_headers(AMZDate, ContentLength, rabbitmq_aws_sign:append_headers(
PayloadHash, Hostname, AMZDate,
SecurityToken, Headers)) ContentLength,
end}, PayloadHash,
{"without security token", fun() -> Hostname,
SecurityToken,
Headers = [{"Content-Type", "application/x-amz-json-1.0"}, Headers
{"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}], )
)
AMZDate = "20160220T120000Z", end},
ContentLength = 128, {"without security token", fun() ->
PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213", Headers = [
Hostname = "ec2.amazonaws.com", {"Content-Type", "application/x-amz-json-1.0"},
Expectation = [{"content-length", integer_to_list(ContentLength)}, {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}
{"content-type", "application/x-amz-json-1.0"}, ],
{"date", AMZDate},
{"host", Hostname},
{"x-amz-content-sha256", PayloadHash},
{"x-amz-target", "DynamoDB_20120810.DescribeTable"}],
?assertEqual(Expectation,
rabbitmq_aws_sign:append_headers(AMZDate, ContentLength,
PayloadHash, Hostname,
undefined, Headers))
end}
].
AMZDate = "20160220T120000Z",
ContentLength = 128,
PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213",
Hostname = "ec2.amazonaws.com",
Expectation = [
{"content-length", integer_to_list(ContentLength)},
{"content-type", "application/x-amz-json-1.0"},
{"date", AMZDate},
{"host", Hostname},
{"x-amz-content-sha256", PayloadHash},
{"x-amz-target", "DynamoDB_20120810.DescribeTable"}
],
?assertEqual(
Expectation,
rabbitmq_aws_sign:append_headers(
AMZDate,
ContentLength,
PayloadHash,
Hostname,
undefined,
Headers
)
)
end}
].
authorization_header_test_() -> authorization_header_test_() ->
[ [
{"value", fun() -> {"value", fun() ->
AccessKey = "AKIDEXAMPLE", AccessKey = "AKIDEXAMPLE",
SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
RequestTimestamp = "20150830T123600Z", RequestTimestamp = "20150830T123600Z",
Region = "us-east-1", Region = "us-east-1",
Service = "iam", Service = "iam",
Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, Headers = [
{"Host", "iam.amazonaws.com"}, {"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
{"Date", "20150830T123600Z"}], {"Host", "iam.amazonaws.com"},
RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", {"Date", "20150830T123600Z"}
Expectation = "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;date;host, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", ],
?assertEqual(Expectation, RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59",
rabbitmq_aws_sign:authorization(AccessKey, SecretKey, RequestTimestamp, Expectation =
Region, Service, Headers, RequestHash)) "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;date;host, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7",
end} ?assertEqual(
]. Expectation,
rabbitmq_aws_sign:authorization(
AccessKey,
SecretKey,
RequestTimestamp,
Region,
Service,
Headers,
RequestHash
)
)
end}
].
canonical_headers_test_() -> canonical_headers_test_() ->
[ [
{"with security token", fun() -> {"with security token", fun() ->
Value = [{"Host", "iam.amazonaws.com"}, Value = [
{"Content-Type", "content-type:application/x-www-form-urlencoded; charset=utf-8"}, {"Host", "iam.amazonaws.com"},
{"My-Header2", "\"a b c \""}, {"Content-Type", "content-type:application/x-www-form-urlencoded; charset=utf-8"},
{"My-Header1", "a b c"}, {"My-Header2", "\"a b c \""},
{"Date", "20150830T123600Z"}], {"My-Header1", "a b c"},
Expectation = lists:flatten([ {"Date", "20150830T123600Z"}
"content-type:content-type:application/x-www-form-urlencoded; charset=utf-8\n", ],
"date:20150830T123600Z\n", Expectation = lists:flatten([
"host:iam.amazonaws.com\n", "content-type:content-type:application/x-www-form-urlencoded; charset=utf-8\n",
"my-header1:a b c\n", "date:20150830T123600Z\n",
"my-header2:\"a b c \"\n"]), "host:iam.amazonaws.com\n",
?assertEqual(Expectation, rabbitmq_aws_sign:canonical_headers(Value)) "my-header1:a b c\n",
end} "my-header2:\"a b c \"\n"
]. ]),
?assertEqual(Expectation, rabbitmq_aws_sign:canonical_headers(Value))
end}
].
credential_scope_test_() -> credential_scope_test_() ->
[ [
{"string value", fun() -> {"string value", fun() ->
RequestDate = "20150830", RequestDate = "20150830",
Region = "us-east-1", Region = "us-east-1",
Service = "iam", Service = "iam",
Expectation = "20150830/us-east-1/iam/aws4_request", Expectation = "20150830/us-east-1/iam/aws4_request",
?assertEqual(Expectation, ?assertEqual(
rabbitmq_aws_sign:credential_scope(RequestDate, Region, Service)) Expectation,
end} rabbitmq_aws_sign:credential_scope(RequestDate, Region, Service)
]. )
end}
].
hmac_sign_test_() -> hmac_sign_test_() ->
[ [
{"signed value", fun() -> {"signed value", fun() ->
?assertEqual([84, 114, 243, 48, 184, 73, 81, 138, 195, 123, 62, 27, 222, 141, 188, 149, 178, 82, 252, 75, 29, 34, 102, 186, 98, 232, 224, 105, 64, 6, 119, 33], ?assertEqual(
rabbitmq_aws_sign:hmac_sign("sixpence", "burn the witch")) [
end} 84,
]. 114,
243,
48,
184,
73,
81,
138,
195,
123,
62,
27,
222,
141,
188,
149,
178,
82,
252,
75,
29,
34,
102,
186,
98,
232,
224,
105,
64,
6,
119,
33
],
rabbitmq_aws_sign:hmac_sign("sixpence", "burn the witch")
)
end}
].
query_string_test_() -> query_string_test_() ->
[ [
{"properly sorted", fun() -> {"properly sorted", fun() ->
QArgs = [{"Version", "2015-10-01"}, QArgs = [
{"Action", "RunInstances"}, {"Version", "2015-10-01"},
{"x-amz-algorithm", "AWS4-HMAC-SHA256"}, {"Action", "RunInstances"},
{"Date", "20160220T120000Z"}, {"x-amz-algorithm", "AWS4-HMAC-SHA256"},
{"x-amz-credential", "AKIDEXAMPLE/20140707/us-east-1/ec2/aws4_request"}], {"Date", "20160220T120000Z"},
Expectation = "Action=RunInstances&Date=20160220T120000Z&Version=2015-10-01&x-amz-algorithm=AWS4-HMAC-SHA256&x-amz-credential=AKIDEXAMPLE%2F20140707%2Fus-east-1%2Fec2%2Faws4_request", {"x-amz-credential", "AKIDEXAMPLE/20140707/us-east-1/ec2/aws4_request"}
?assertEqual(Expectation, ],
rabbitmq_aws_sign:query_string(QArgs)) Expectation =
end}, "Action=RunInstances&Date=20160220T120000Z&Version=2015-10-01&x-amz-algorithm=AWS4-HMAC-SHA256&x-amz-credential=AKIDEXAMPLE%2F20140707%2Fus-east-1%2Fec2%2Faws4_request",
{"undefined", fun() -> ?assertEqual(
?assertEqual([], rabbitmq_aws_sign:query_string(undefined)) Expectation,
end} rabbitmq_aws_sign:query_string(QArgs)
]. )
end},
{"undefined", fun() ->
?assertEqual([], rabbitmq_aws_sign:query_string(undefined))
end}
].
request_hash_test_() -> request_hash_test_() ->
[ [
{"hash value", fun() -> {"hash value", fun() ->
Method = get, Method = get,
Path = "/", Path = "/",
QArgs = [{"Action", "ListUsers"}, {"Version", "2010-05-08"}], QArgs = [{"Action", "ListUsers"}, {"Version", "2010-05-08"}],
Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, Headers = [
{"Host", "iam.amazonaws.com"}, {"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
{"Date", "20150830T123600Z"}], {"Host", "iam.amazonaws.com"},
Payload = "", {"Date", "20150830T123600Z"}
Expectation = "49b454e0f20fe17f437eaa570846fc5d687efc1752c8b5a1eeee5597a7eb92a5", ],
?assertEqual(Expectation, Payload = "",
rabbitmq_aws_sign:request_hash(Method, Path, QArgs, Headers, Payload)) Expectation = "49b454e0f20fe17f437eaa570846fc5d687efc1752c8b5a1eeee5597a7eb92a5",
end} ?assertEqual(
]. Expectation,
rabbitmq_aws_sign:request_hash(Method, Path, QArgs, Headers, Payload)
)
end}
].
signature_test_() -> signature_test_() ->
[ [
{"value", fun() -> {"value", fun() ->
StringToSign = "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", StringToSign =
SigningKey = [196, 175, 177, 204, 87, 113, 216, 113, 118, 58, 57, 62, 68, 183, 3, 87, 27, 85, 204, 40, 66, 77, 26, 94, 134, 218, 110, 211, 193, 84, 164, 185], "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59",
Expectation = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", SigningKey = [
?assertEqual(Expectation, rabbitmq_aws_sign:signature(StringToSign, SigningKey)) 196,
end} 175,
]. 177,
204,
87,
113,
216,
113,
118,
58,
57,
62,
68,
183,
3,
87,
27,
85,
204,
40,
66,
77,
26,
94,
134,
218,
110,
211,
193,
84,
164,
185
],
Expectation = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7",
?assertEqual(Expectation, rabbitmq_aws_sign:signature(StringToSign, SigningKey))
end}
].
signed_headers_test_() -> signed_headers_test_() ->
[ [
{"with security token", fun() -> {"with security token", fun() ->
Value = [{"X-Amz-Security-Token", "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"}, Value = [
{"Date", "20160220T120000Z"}, {"X-Amz-Security-Token",
{"Content-Type", "application/x-amz-json-1.0"}, "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"},
{"Host", "ec2.amazonaws.com"}, {"Date", "20160220T120000Z"},
{"Content-Length", 128}, {"Content-Type", "application/x-amz-json-1.0"},
{"X-Amz-Content-sha256", "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213"}, {"Host", "ec2.amazonaws.com"},
{"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}], {"Content-Length", 128},
Expectation = "content-length;content-type;date;host;x-amz-content-sha256;x-amz-security-token;x-amz-target", {"X-Amz-Content-sha256",
?assertEqual(Expectation, rabbitmq_aws_sign:signed_headers(Value)) "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213"},
end} {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}
]. ],
Expectation =
"content-length;content-type;date;host;x-amz-content-sha256;x-amz-security-token;x-amz-target",
?assertEqual(Expectation, rabbitmq_aws_sign:signed_headers(Value))
end}
].
signing_key_test_() -> signing_key_test_() ->
[ [
{"signing key value", fun() -> {"signing key value", fun() ->
SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
AMZDate = "20150830", AMZDate = "20150830",
Region = "us-east-1", Region = "us-east-1",
Service = "iam", Service = "iam",
Expectation = [196, 175, 177, 204, 87, 113, 216, 113, 118, 58, 57, 62, 68, 183, 3, 87, 27, 85, 204, 40, 66, 77, 26, 94, 134, 218, 110, 211, 193, 84, 164, 185], Expectation = [
?assertEqual(Expectation, 196,
rabbitmq_aws_sign:signing_key(SecretKey, AMZDate, Region, Service)) 175,
end} 177,
]. 204,
87,
113,
216,
113,
118,
58,
57,
62,
68,
183,
3,
87,
27,
85,
204,
40,
66,
77,
26,
94,
134,
218,
110,
211,
193,
84,
164,
185
],
?assertEqual(
Expectation,
rabbitmq_aws_sign:signing_key(SecretKey, AMZDate, Region, Service)
)
end}
].
string_to_sign_test_() -> string_to_sign_test_() ->
[ [
{"string value", fun() -> {"string value", fun() ->
RequestTimestamp = "20150830T123600Z", RequestTimestamp = "20150830T123600Z",
RequestDate = "20150830", RequestDate = "20150830",
Region = "us-east-1", Region = "us-east-1",
Service = "iam", Service = "iam",
RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59",
Expectation = "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", Expectation =
?assertEqual(Expectation, "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59",
rabbitmq_aws_sign:string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash)) ?assertEqual(
end} Expectation,
]. rabbitmq_aws_sign:string_to_sign(
RequestTimestamp, RequestDate, Region, Service, RequestHash
)
)
end}
].
local_time_0_test_() -> local_time_0_test_() ->
{foreach, {foreach,
fun() -> fun() ->
meck:new(calendar, [passthrough, unstick]) meck:new(calendar, [passthrough, unstick])
end, end,
fun(_) -> fun(_) ->
meck:unload(calendar) meck:unload(calendar)
end, end,
[ [
{"variation1", fun() -> {"variation1", fun() ->
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 05, 08}, {12, 36, 00}}] end), meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
Expectation = "20150508T123600Z", [{{2015, 05, 08}, {12, 36, 00}}]
?assertEqual(Expectation, rabbitmq_aws_sign:local_time()), end),
meck:validate(calendar) Expectation = "20150508T123600Z",
end} ?assertEqual(Expectation, rabbitmq_aws_sign:local_time()),
]}. meck:validate(calendar)
end}
]}.
local_time_1_test_() -> local_time_1_test_() ->
[ [
{"variation1", fun() -> {"variation1", fun() ->
Value = {{2015, 05, 08}, {13, 15, 20}}, Value = {{2015, 05, 08}, {13, 15, 20}},
Expectation = "20150508T131520Z", Expectation = "20150508T131520Z",
?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value))
end}, end},
{"variation2", fun() -> {"variation2", fun() ->
Value = {{2015, 05, 08}, {06, 07, 08}}, Value = {{2015, 05, 08}, {06, 07, 08}},
Expectation = "20150508T060708Z", Expectation = "20150508T060708Z",
?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value))
end} end}
]. ].
headers_test_() -> headers_test_() ->
{foreach, {foreach,
fun() -> fun() ->
meck:new(calendar, [passthrough, unstick]) meck:new(calendar, [passthrough, unstick])
end, end,
fun(_) -> fun(_) ->
meck:unload(calendar) meck:unload(calendar)
end, end,
[ [
{"without signing key", fun() -> {"without signing key", fun() ->
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 08, 30}, {12, 36, 00}}] end), meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
Request = #request{ [{{2015, 08, 30}, {12, 36, 00}}]
access_key = "AKIDEXAMPLE", end),
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", Request = #request{
service = "iam", access_key = "AKIDEXAMPLE",
method = get, secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1", service = "iam",
uri = "https://iam.amazonaws.com/?Action=ListUsers&Version=2015-05-08", method = get,
body = "", region = "us-east-1",
headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}]}, uri = "https://iam.amazonaws.com/?Action=ListUsers&Version=2015-05-08",
Expectation = [ body = "",
{"authorization", "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-length;content-type;date;host;x-amz-content-sha256, Signature=81cb49e1e232a0a5f7f594ad6b2ad2b8b7adbafddb3604d00491fe8f3cc5a442"}, headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}]
{"content-length", "0"}, },
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"}, Expectation = [
{"date", "20150830T123600Z"}, {"authorization",
{"host", "iam.amazonaws.com"}, "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-length;content-type;date;host;x-amz-content-sha256, Signature=81cb49e1e232a0a5f7f594ad6b2ad2b8b7adbafddb3604d00491fe8f3cc5a442"},
{"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} {"content-length", "0"},
], {"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)), {"date", "20150830T123600Z"},
meck:validate(calendar) {"host", "iam.amazonaws.com"},
end}, {"x-amz-content-sha256",
{"with host header", fun() -> "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 08, 30}, {12, 36, 00}}] end), ],
Request = #request{ ?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)),
access_key = "AKIDEXAMPLE", meck:validate(calendar)
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", end},
service = "iam", {"with host header", fun() ->
method = get, meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
region = "us-east-1", [{{2015, 08, 30}, {12, 36, 00}}]
uri = "https://s3.us-east-1.amazonaws.com/?list-type=2", end),
body = "", Request = #request{
headers = [{"host", "gavinroy.com.s3.amazonaws.com"}]}, access_key = "AKIDEXAMPLE",
Expectation = [ secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
{"authorization", "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-length;date;host;x-amz-content-sha256, Signature=64e549daad14fc1ba9fc4aca6b7df4b2c60e352e3313090d84a2941c1e653d36"}, service = "iam",
{"content-length","0"}, method = get,
{"date","20150830T123600Z"}, region = "us-east-1",
{"host","gavinroy.com.s3.amazonaws.com"}, uri = "https://s3.us-east-1.amazonaws.com/?list-type=2",
{"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} body = "",
], headers = [{"host", "gavinroy.com.s3.amazonaws.com"}]
?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)), },
meck:validate(calendar) Expectation = [
end} {"authorization",
] "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-length;date;host;x-amz-content-sha256, Signature=64e549daad14fc1ba9fc4aca6b7df4b2c60e352e3313090d84a2941c1e653d36"},
}. {"content-length", "0"},
{"date", "20150830T123600Z"},
{"host", "gavinroy.com.s3.amazonaws.com"},
{"x-amz-content-sha256",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
],
?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)),
meck:validate(calendar)
end}
]}.

View File

@ -3,25 +3,29 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
start_link_test_() -> start_link_test_() ->
{foreach, {foreach,
fun() -> fun() ->
meck:new(supervisor, [passthrough, unstick]) meck:new(supervisor, [passthrough, unstick])
end, end,
fun(_) -> fun(_) ->
meck:unload(supervisor) meck:unload(supervisor)
end, end,
[ [
{"supervisor start_link", fun() -> {"supervisor start_link", fun() ->
meck:expect(supervisor, start_link, fun(_, _, _) -> {ok, test_result} end), meck:expect(supervisor, start_link, fun(_, _, _) -> {ok, test_result} end),
?assertEqual({ok, test_result}, ?assertEqual(
rabbitmq_aws_sup:start_link()), {ok, test_result},
meck:validate(supervisor) rabbitmq_aws_sup:start_link()
end} ),
] meck:validate(supervisor)
}. end}
]}.
init_test() -> init_test() ->
?assertEqual({ok, {{one_for_one, 5, 10}, ?assertEqual(
[{rabbitmq_aws, {rabbitmq_aws, start_link, []}, {ok,
permanent, 5, worker, [rabbitmq_aws]}]}}, {{one_for_one, 5, 10}, [
rabbitmq_aws_sup:init([])). {rabbitmq_aws, {rabbitmq_aws, start_link, []}, permanent, 5, worker, [rabbitmq_aws]}
]}},
rabbitmq_aws_sup:init([])
).

File diff suppressed because it is too large Load Diff

View File

@ -5,150 +5,181 @@
-include("rabbitmq_aws.hrl"). -include("rabbitmq_aws.hrl").
build_test_() -> build_test_() ->
[ [
{"variation1", fun() -> {"variation1", fun() ->
Expect = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", Expect = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5",
Value = #uri{scheme = "amqp", Value = #uri{
authority = {{"guest", "password"}, "rabbitmq", 5672}, scheme = "amqp",
path = "/%2F", query = [{"heartbeat", "5"}]}, authority = {{"guest", "password"}, "rabbitmq", 5672},
Result = rabbitmq_aws_urilib:build(Value), path = "/%2F",
?assertEqual(Expect, Result) query = [{"heartbeat", "5"}]
end}, },
{"variation2", fun() -> Result = rabbitmq_aws_urilib:build(Value),
Expect = "http://www.google.com:80/search?foo=bar#baz", ?assertEqual(Expect, Result)
Value = #uri{scheme = http, end},
authority = {undefined, "www.google.com", 80}, {"variation2", fun() ->
path = "/search", Expect = "http://www.google.com:80/search?foo=bar#baz",
query = [{"foo", "bar"}], Value = #uri{
fragment = "baz"}, scheme = http,
Result = rabbitmq_aws_urilib:build(Value), authority = {undefined, "www.google.com", 80},
?assertEqual(Expect, Result) path = "/search",
end}, query = [{"foo", "bar"}],
{"variation3", fun() -> fragment = "baz"
Expect = "https://www.google.com/search", },
Value = #uri{scheme = "https", Result = rabbitmq_aws_urilib:build(Value),
authority = {undefined, "www.google.com", undefined}, ?assertEqual(Expect, Result)
path = "/search"}, end},
Result = rabbitmq_aws_urilib:build(Value), {"variation3", fun() ->
?assertEqual(Expect, Result) Expect = "https://www.google.com/search",
end}, Value = #uri{
{"variation5", fun() -> scheme = "https",
Expect = "https://www.google.com:443/search?foo=true", authority = {undefined, "www.google.com", undefined},
Value = #uri{scheme = "https", path = "/search"
authority = {undefined, "www.google.com", 443}, },
path = "/search", Result = rabbitmq_aws_urilib:build(Value),
query = [{"foo", true}]}, ?assertEqual(Expect, Result)
Result = rabbitmq_aws_urilib:build(Value), end},
?assertEqual(Expect, Result) {"variation5", fun() ->
end}, Expect = "https://www.google.com:443/search?foo=true",
{"variation6", fun() -> Value = #uri{
Expect = "https://bar@www.google.com:443/search?foo=true", scheme = "https",
Value = #uri{scheme = "https", authority = {undefined, "www.google.com", 443},
authority = {{"bar", undefined}, "www.google.com", 443}, path = "/search",
path = "/search", query = [{"foo", true}]
query = [{"foo", true}]}, },
Result = rabbitmq_aws_urilib:build(Value), Result = rabbitmq_aws_urilib:build(Value),
?assertEqual(Expect, Result) ?assertEqual(Expect, Result)
end}, end},
{"variation7", fun() -> {"variation6", fun() ->
Expect = "https://www.google.com:443/search?foo=true", Expect = "https://bar@www.google.com:443/search?foo=true",
Value = #uri{scheme = "https", Value = #uri{
authority = {undefined, "www.google.com", 443}, scheme = "https",
path = "/search", authority = {{"bar", undefined}, "www.google.com", 443},
query = [{"foo", true}]}, path = "/search",
Result = rabbitmq_aws_urilib:build(Value), query = [{"foo", true}]
?assertEqual(Expect, Result) },
end}, Result = rabbitmq_aws_urilib:build(Value),
{"variation8", fun() -> ?assertEqual(Expect, Result)
Expect = "https://:@www.google.com:443/search?foo=true", end},
Value = #uri{scheme = "https", {"variation7", fun() ->
authority = {{"", ""}, "www.google.com", 443}, Expect = "https://www.google.com:443/search?foo=true",
path = "/search", Value = #uri{
query = [{"foo", true}]}, scheme = "https",
Result = rabbitmq_aws_urilib:build(Value), authority = {undefined, "www.google.com", 443},
?assertEqual(Expect, Result) path = "/search",
end}, query = [{"foo", true}]
{"variation9", fun() -> },
Expect = "https://bar:@www.google.com:443/search?foo=true#", Result = rabbitmq_aws_urilib:build(Value),
Value = #uri{scheme = "https", ?assertEqual(Expect, Result)
authority={{"bar", ""}, "www.google.com", 443}, end},
path="/search", {"variation8", fun() ->
query=[{"foo", true}], Expect = "https://:@www.google.com:443/search?foo=true",
fragment=""}, Value = #uri{
Result = rabbitmq_aws_urilib:build(Value), scheme = "https",
?assertEqual(Expect, Result) authority = {{"", ""}, "www.google.com", 443},
end}, path = "/search",
{"variation10", fun() -> query = [{"foo", true}]
Expect = "http://www.google.com/search?foo=true#bar", },
Value = #uri{scheme = "http", Result = rabbitmq_aws_urilib:build(Value),
authority = {undefined, "www.google.com", undefined}, ?assertEqual(Expect, Result)
path = "/search", end},
query = [{"foo", true}], {"variation9", fun() ->
fragment = "bar"}, Expect = "https://bar:@www.google.com:443/search?foo=true#",
Result = rabbitmq_aws_urilib:build(Value), Value = #uri{
?assertEqual(Expect, Result) scheme = "https",
end}, authority = {{"bar", ""}, "www.google.com", 443},
{"variation11", fun() -> path = "/search",
Expect = "http://www.google.com", query = [{"foo", true}],
Value = #uri{scheme = "http", fragment = ""
authority = {undefined, "www.google.com", undefined}, },
path = undefined, Result = rabbitmq_aws_urilib:build(Value),
query = []}, ?assertEqual(Expect, Result)
Result = rabbitmq_aws_urilib:build(Value), end},
?assertEqual(Expect, Result) {"variation10", fun() ->
end} Expect = "http://www.google.com/search?foo=true#bar",
]. Value = #uri{
scheme = "http",
authority = {undefined, "www.google.com", undefined},
path = "/search",
query = [{"foo", true}],
fragment = "bar"
},
Result = rabbitmq_aws_urilib:build(Value),
?assertEqual(Expect, Result)
end},
{"variation11", fun() ->
Expect = "http://www.google.com",
Value = #uri{
scheme = "http",
authority = {undefined, "www.google.com", undefined},
path = undefined,
query = []
},
Result = rabbitmq_aws_urilib:build(Value),
?assertEqual(Expect, Result)
end}
].
build_query_string_test_() -> build_query_string_test_() ->
[ [
{"basic list", fun() -> {"basic list", fun() ->
?assertEqual("foo=bar&baz=qux", ?assertEqual(
rabbitmq_aws_urilib:build_query_string([{"foo", "bar"}, "foo=bar&baz=qux",
{"baz", "qux"}])) rabbitmq_aws_urilib:build_query_string([
end}, {"foo", "bar"},
{"empty list", fun() -> {"baz", "qux"}
?assertEqual("", rabbitmq_aws_urilib:build_query_string([])) ])
end} )
]. end},
{"empty list", fun() ->
?assertEqual("", rabbitmq_aws_urilib:build_query_string([]))
end}
].
parse_test_() -> parse_test_() ->
[ [
{"variation1", fun() -> {"variation1", fun() ->
URI = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", URI = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5",
Expect = #uri{scheme = "amqp", Expect = #uri{
authority = {{"guest", "password"}, "rabbitmq", 5672}, scheme = "amqp",
path = "/%2F", authority = {{"guest", "password"}, "rabbitmq", 5672},
query = [{"heartbeat", "5"}], path = "/%2F",
fragment = undefined}, query = [{"heartbeat", "5"}],
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) fragment = undefined
end}, },
{"variation2", fun() -> ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
URI = "http://www.google.com/search?foo=bar#baz", end},
Expect = #uri{scheme = "http", {"variation2", fun() ->
authority = {undefined, "www.google.com", 80}, URI = "http://www.google.com/search?foo=bar#baz",
path = "/search", Expect = #uri{
query = [{"foo", "bar"}], scheme = "http",
fragment = "baz"}, authority = {undefined, "www.google.com", 80},
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) path = "/search",
end}, query = [{"foo", "bar"}],
{"variation3", fun() -> fragment = "baz"
URI = "https://www.google.com/search", },
Expect = #uri{scheme = "https", ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
authority = {undefined, "www.google.com", 443}, end},
path = "/search", {"variation3", fun() ->
query = "", URI = "https://www.google.com/search",
fragment = undefined}, Expect = #uri{
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) scheme = "https",
end}, authority = {undefined, "www.google.com", 443},
{"variation4", fun() -> path = "/search",
URI = "https://www.google.com/search?foo=true", query = "",
Expect = #uri{scheme = "https", fragment = undefined
authority = {undefined, "www.google.com", 443}, },
path = "/search", ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
query = [{"foo", "true"}], end},
fragment = undefined}, {"variation4", fun() ->
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) URI = "https://www.google.com/search?foo=true",
end} Expect = #uri{
]. scheme = "https",
authority = {undefined, "www.google.com", 443},
path = "/search",
query = [{"foo", "true"}],
fragment = undefined
},
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
end}
].

View File

@ -3,36 +3,48 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
parse_test_() -> parse_test_() ->
[ [
{"s3 error response", fun() -> {"s3 error response", fun() ->
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIAIPPU25E5RA4MIYKQ</AWSAccessKeyId><StringToSign>AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7</StringToSign><SignatureProvided>841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e</SignatureProvided><StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37</StringToSignBytes><CanonicalRequest>GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</CanonicalRequest><CanonicalRequestBytes>47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35</CanonicalRequestBytes><RequestId>8EB36F450B78C45D</RequestId><HostId>IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=</HostId></Error>", Response =
Expectation = [{"Error", [ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIAIPPU25E5RA4MIYKQ</AWSAccessKeyId><StringToSign>AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7</StringToSign><SignatureProvided>841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e</SignatureProvided><StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37</StringToSignBytes><CanonicalRequest>GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</CanonicalRequest><CanonicalRequestBytes>47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35</CanonicalRequestBytes><RequestId>8EB36F450B78C45D</RequestId><HostId>IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=</HostId></Error>",
{"Code", "SignatureDoesNotMatch"}, Expectation = [
{"Message", "The request signature we calculated does not match the signature you provided. Check your key and signing method."}, {"Error", [
{"AWSAccessKeyId", "AKIAIPPU25E5RA4MIYKQ"}, {"Code", "SignatureDoesNotMatch"},
{"StringToSign", "AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7"}, {"Message",
{"SignatureProvided", "841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e"}, "The request signature we calculated does not match the signature you provided. Check your key and signing method."},
{"StringToSignBytes", "41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37"}, {"AWSAccessKeyId", "AKIAIPPU25E5RA4MIYKQ"},
{"CanonicalRequest", "GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"StringToSign",
{"CanonicalRequestBytes", "47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35"}, "AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7"},
{"RequestId","8EB36F450B78C45D"}, {"SignatureProvided",
{"HostId", "IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw="} "841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e"},
]}], {"StringToSignBytes",
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) "41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37"},
end}, {"CanonicalRequest",
{"whitespace", fun() -> "GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test> <example> value</example>\n</test> \n", {"CanonicalRequestBytes",
Expectation = [{"test", [{"example", "value"}]}], "47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35"},
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) {"RequestId", "8EB36F450B78C45D"},
end}, {"HostId",
{"multiple items", fun() -> "IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw="}
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test><values><example>value</example><example>value2</example></values>\n</test> \n", ]}
Expectation = [{"test", [{"values", [{"example", "value"}, {"example", "value2"}]}]}], ],
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
end}, end},
{"small snippert", fun() -> {"whitespace", fun() ->
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test>value</test>", Response =
Expectation = [{"test", "value"}], "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test> <example> value</example>\n</test> \n",
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) Expectation = [{"test", [{"example", "value"}]}],
end} ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
]. end},
{"multiple items", fun() ->
Response =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test><values><example>value</example><example>value2</example></values>\n</test> \n",
Expectation = [{"test", [{"values", [{"example", "value"}, {"example", "value2"}]}]}],
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
end},
{"small snippert", fun() ->
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test>value</test>",
Expectation = [{"test", "value"}],
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
end}
].