From a4fffbd7e0a312fef2e514ade54fc4310a681542 Mon Sep 17 00:00:00 2001 From: Simon Unge Date: Tue, 29 Jul 2025 17:16:27 +0000 Subject: [PATCH 1/2] erlfmt entire plugin --- deps/rabbitmq_aws/include/rabbitmq_aws.hrl | 96 +- deps/rabbitmq_aws/src/rabbitmq_aws.erl | 633 ++++---- deps/rabbitmq_aws/src/rabbitmq_aws_app.erl | 4 +- deps/rabbitmq_aws/src/rabbitmq_aws_config.erl | 670 +++++---- deps/rabbitmq_aws/src/rabbitmq_aws_json.erl | 78 +- deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl | 366 ++--- deps/rabbitmq_aws/src/rabbitmq_aws_sup.erl | 10 +- deps/rabbitmq_aws/src/rabbitmq_aws_urilib.erl | 151 +- deps/rabbitmq_aws/src/rabbitmq_aws_xml.erl | 41 +- .../test/rabbitmq_aws_all_tests.erl | 22 +- .../test/rabbitmq_aws_app_tests.erl | 35 +- .../test/rabbitmq_aws_config_tests.erl | 893 +++++++----- .../test/rabbitmq_aws_json_tests.erl | 156 +- .../test/rabbitmq_aws_sign_tests.erl | 678 +++++---- .../test/rabbitmq_aws_sup_tests.erl | 44 +- deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl | 1284 +++++++++-------- .../test/rabbitmq_aws_urilib_tests.erl | 317 ++-- .../test/rabbitmq_aws_xml_tests.erl | 78 +- 18 files changed, 3090 insertions(+), 2466 deletions(-) diff --git a/deps/rabbitmq_aws/include/rabbitmq_aws.hrl b/deps/rabbitmq_aws/include/rabbitmq_aws.hrl index ab16d9ed49f4..6a0cacd81131 100644 --- a/deps/rabbitmq_aws/include/rabbitmq_aws.hrl +++ b/deps/rabbitmq_aws/include/rabbitmq_aws.hrl @@ -57,18 +57,22 @@ -type sc_error() :: {error, Reason :: atom()}. -type security_credentials() :: sc_ok() | sc_error(). --record(imdsv2token, { token :: security_token() | undefined, - expiration :: non_neg_integer() | undefined}). +-record(imdsv2token, { + token :: security_token() | undefined, + expiration :: non_neg_integer() | undefined +}). -type imdsv2token() :: #imdsv2token{}. --record(state, {access_key :: access_key() | undefined, - secret_access_key :: secret_access_key() | undefined, - expiration :: expiration() | undefined, - security_token :: security_token() | undefined, - region :: region() | undefined, - imdsv2_token:: imdsv2token() | undefined, - error :: atom() | string() | undefined}). +-record(state, { + access_key :: access_key() | undefined, + secret_access_key :: secret_access_key() | undefined, + expiration :: expiration() | undefined, + security_token :: security_token() | undefined, + region :: region() | undefined, + imdsv2_token :: imdsv2token() | undefined, + error :: atom() | string() | undefined +}). -type state() :: #state{}. -type scheme() :: atom(). @@ -79,17 +83,16 @@ -type query_args() :: [tuple() | string()]. -type fragment() :: string(). --type userinfo() :: {undefined | username(), - undefined | password()}. +-type userinfo() :: {undefined | username(), undefined | password()}. --type authority() :: {undefined | userinfo(), - host(), - undefined | tcp_port()}. --record(uri, {scheme :: undefined | scheme(), - authority :: authority(), - path :: undefined | path(), - query :: undefined | query_args(), - fragment :: undefined | fragment()}). +-type authority() :: {undefined | userinfo(), host(), undefined | tcp_port()}. +-record(uri, { + scheme :: undefined | scheme(), + authority :: authority(), + path :: undefined | path(), + query :: undefined | query_args(), + fragment :: undefined | fragment() +}). -type method() :: head | get | put | post | trace | options | delete | patch. -type http_version() :: string(). @@ -104,35 +107,40 @@ -type ssl_options() :: [ssl:tls_client_option()]. --type http_option() :: {timeout, timeout()} | - {connect_timeout, timeout()} | - {ssl, ssl_options()} | - {essl, ssl_options()} | - {autoredirect, boolean()} | - {proxy_auth, {User :: string(), Password :: string()}} | - {version, http_version()} | - {relaxed, boolean()} | - {url_encode, boolean()}. +-type http_option() :: + {timeout, timeout()} + | {connect_timeout, timeout()} + | {ssl, ssl_options()} + | {essl, ssl_options()} + | {autoredirect, boolean()} + | {proxy_auth, {User :: string(), Password :: string()}} + | {version, http_version()} + | {relaxed, boolean()} + | {url_encode, boolean()}. -type http_options() :: [http_option()]. - --record(request, {access_key :: access_key(), - secret_access_key :: secret_access_key(), - security_token :: security_token(), - service :: string(), - region = "us-east-1" :: string(), - method = get :: method(), - headers = [] :: headers(), - uri :: string(), - body = "" :: body()}). +-record(request, { + access_key :: access_key(), + secret_access_key :: secret_access_key(), + security_token :: security_token(), + service :: string(), + region = "us-east-1" :: string(), + method = get :: method(), + headers = [] :: headers(), + uri :: string(), + body = "" :: body() +}). -type request() :: #request{}. --type httpc_result() :: {ok, {status_line(), headers(), body()}} | - {ok, {status_code(), body()}} | - {error, term()}. +-type httpc_result() :: + {ok, {status_line(), headers(), body()}} + | {ok, {status_code(), body()}} + | {error, term()}. -type result_ok() :: {ok, {ResponseHeaders :: headers(), Response :: list()}}. --type result_error() :: {'error', Message :: reason_phrase(), {ResponseHeaders :: headers(), Response :: list()} | undefined} | - {'error', {credentials, Reason :: string()}} | - {'error', string()}. +-type result_error() :: + {'error', Message :: reason_phrase(), + {ResponseHeaders :: headers(), Response :: list()} | undefined} + | {'error', {credentials, Reason :: string()}} + | {'error', string()}. -type result() :: result_ok() | result_error(). diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws.erl b/deps/rabbitmq_aws/src/rabbitmq_aws.erl index 5a45a597d851..e0c85ec55372 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws.erl @@ -9,24 +9,28 @@ -behavior(gen_server). %% API exports --export([get/2, get/3, - post/4, - refresh_credentials/0, - request/5, request/6, request/7, - set_credentials/2, - has_credentials/0, - set_region/1, - ensure_imdsv2_token_valid/0, - api_get_request/2]). +-export([ + get/2, get/3, + post/4, + refresh_credentials/0, + request/5, request/6, request/7, + set_credentials/2, + has_credentials/0, + set_region/1, + ensure_imdsv2_token_valid/0, + api_get_request/2 +]). %% gen-server exports --export([start_link/0, - init/1, - terminate/2, - code_change/3, - handle_call/3, - handle_cast/2, - handle_info/2]). +-export([ + start_link/0, + init/1, + terminate/2, + code_change/3, + handle_call/3, + handle_cast/2, + handle_info/2 +]). %% Export all for unit tests -ifdef(TEST). @@ -40,101 +44,110 @@ %% exported wrapper functions %%==================================================================== --spec get(Service :: string(), - Path :: path()) -> result(). +-spec get( + Service :: string(), + Path :: path() +) -> result(). %% @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 %% format. %% @end get(Service, Path) -> - get(Service, Path, []). + get(Service, Path, []). - --spec get(Service :: string(), - Path :: path(), - Headers :: headers()) -> result(). +-spec get( + Service :: string(), + Path :: path(), + Headers :: headers() +) -> result(). %% @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 %% format. %% @end get(Service, Path, Headers) -> - request(Service, get, Path, "", Headers). - - --spec post(Service :: string(), - Path :: path(), - Body :: body(), - Headers :: headers()) -> result(). + request(Service, get, Path, "", Headers). + +-spec post( + Service :: string(), + Path :: path(), + Body :: body(), + Headers :: headers() +) -> result(). %% @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 %% format. %% @end post(Service, Path, Body, Headers) -> - request(Service, post, Path, Body, Headers). - + request(Service, post, Path, Body, Headers). -spec refresh_credentials() -> ok | error. %% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service. %% @end refresh_credentials() -> - gen_server:call(rabbitmq_aws, refresh_credentials). - + gen_server:call(rabbitmq_aws, refresh_credentials). -spec refresh_credentials(state()) -> ok | error. %% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service. %% @end refresh_credentials(State) -> - ?LOG_DEBUG("Refreshing AWS credentials..."), - {_, NewState} = load_credentials(State), - ?LOG_DEBUG("AWS credentials have been refreshed"), - set_credentials(NewState). - - --spec request(Service :: string(), - Method :: method(), - Path :: path(), - Body :: body(), - Headers :: headers()) -> result(). + ?LOG_DEBUG("Refreshing AWS credentials..."), + {_, NewState} = load_credentials(State), + ?LOG_DEBUG("AWS credentials have been refreshed"), + set_credentials(NewState). + +-spec request( + Service :: string(), + Method :: method(), + Path :: path(), + Body :: body(), + Headers :: headers() +) -> result(). %% @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 %% format. %% @end request(Service, Method, Path, Body, Headers) -> - gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, [], undefined}). - - --spec request(Service :: string(), - Method :: method(), - Path :: path(), - Body :: body(), - Headers :: headers(), - HTTPOptions :: http_options()) -> result(). + gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, [], undefined}). + +-spec request( + Service :: string(), + Method :: method(), + Path :: path(), + Body :: body(), + Headers :: headers(), + HTTPOptions :: http_options() +) -> result(). %% @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 %% format. %% @end request(Service, Method, Path, Body, Headers, HTTPOptions) -> - gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, undefined}). - - --spec request(Service :: string(), - Method :: method(), - Path :: path(), - Body :: body(), - Headers :: headers(), - HTTPOptions :: http_options(), - Endpoint :: host()) -> result(). + gen_server:call( + rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, undefined} + ). + +-spec request( + Service :: string(), + Method :: method(), + Path :: path(), + Body :: body(), + Headers :: headers(), + HTTPOptions :: http_options(), + Endpoint :: host() +) -> result(). %% @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 %% of services such as DynamoDB. The response will automatically be decoded %% if it is either in JSON or XML format. %% @end 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. 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. %% @doc Manually set the access credentials for requests. This should @@ -143,122 +156,113 @@ set_credentials(NewState) -> %% configuration or the AWS Instance Metadata service. %% @end 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. %% @doc Manually set the AWS region to perform API requests to. %% @end 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. %% @doc Manually set the Imdsv2Token used to perform instance metadata service requests. %% @end 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'. %% @doc return the current Imdsv2Token used to perform instance metadata service requests. %% @end get_imdsv2_token() -> - {ok, Imdsv2Token} = gen_server:call(rabbitmq_aws, get_imdsv2_token), - Imdsv2Token. - + {ok, Imdsv2Token} = gen_server:call(rabbitmq_aws, get_imdsv2_token), + Imdsv2Token. %%==================================================================== %% gen_server functions %%==================================================================== start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec init(list()) -> {ok, state()}. init([]) -> - {ok, #state{}}. - + {ok, #state{}}. terminate(_, _) -> - ok. - + ok. code_change(_, _, State) -> - {ok, State}. + {ok, State}. handle_call(Msg, _From, State) -> - handle_msg(Msg, State). + handle_msg(Msg, State). handle_cast(_Request, State) -> - {noreply, State}. - + {noreply, State}. handle_info(_Info, State) -> - {noreply, State}. + {noreply, State}. %%==================================================================== %% Internal functions %%==================================================================== 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}; - handle_msg(get_state, State) -> {reply, {ok, State}, State}; - handle_msg(refresh_credentials, State) -> {Reply, NewState} = load_credentials(State), {reply, Reply, NewState}; - handle_msg({set_credentials, AccessKey, SecretAccessKey}, State) -> - {reply, ok, State#state{access_key = AccessKey, - secret_access_key = SecretAccessKey, - security_token = undefined, - expiration = undefined, - error = undefined}}; - + {reply, ok, State#state{ + access_key = AccessKey, + secret_access_key = SecretAccessKey, + security_token = undefined, + expiration = undefined, + error = undefined + }}; handle_msg({set_credentials, NewState}, State) -> - {reply, ok, State#state{access_key = NewState#state.access_key, - secret_access_key = NewState#state.secret_access_key, - security_token = NewState#state.security_token, - expiration = NewState#state.expiration, - error = NewState#state.error}}; - + {reply, ok, State#state{ + access_key = NewState#state.access_key, + secret_access_key = NewState#state.secret_access_key, + security_token = NewState#state.security_token, + expiration = NewState#state.expiration, + error = NewState#state.error + }}; handle_msg({set_region, Region}, State) -> {reply, ok, State#state{region = Region}}; - handle_msg({set_imdsv2_token, Imdsv2Token}, State) -> {reply, ok, State#state{imdsv2_token = Imdsv2Token}}; - handle_msg(has_credentials, State) -> {reply, has_credentials(State), State}; - handle_msg(get_imdsv2_token, State) -> {reply, {ok, State#state.imdsv2_token}, State}; - handle_msg(_Request, State) -> {noreply, State}. - --spec endpoint(State :: state(), Host :: string(), - Service :: string(), Path :: string()) -> string(). +-spec endpoint( + State :: state(), + Host :: string(), + Service :: string(), + Path :: string() +) -> string(). %% @doc Return the endpoint URL, either by constructing it with the service %% information passed in, or by using the passed in Host value. %% @ednd 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) -> - lists:flatten(["https://", Host, Path]). - + lists:flatten(["https://", Host, Path]). -spec endpoint_host(Region :: region(), Service :: string()) -> host(). %% @doc Construct the endpoint hostname for the request based upon the service %% and region. %% @end 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(). %% @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. %% @end 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 -> - {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}) -> - {error, Reason, undefined}. + {error, Reason, undefined}. -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 %% {Type, Subtype}. %% @end get_content_type(Headers) -> - Value = case proplists:get_value("content-type", Headers, undefined) of - undefined -> - proplists:get_value("Content-Type", Headers, "text/xml"); - Other -> Other - end, - parse_content_type(Value). + Value = + case proplists:get_value("content-type", Headers, undefined) of + undefined -> + proplists:get_value("Content-Type", Headers, "text/xml"); + Other -> + Other + end, + parse_content_type(Value). -spec has_credentials() -> boolean(). has_credentials() -> - gen_server:call(rabbitmq_aws, has_credentials). + gen_server:call(rabbitmq_aws, has_credentials). -spec has_credentials(state()) -> boolean(). %% @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(_) -> false. - -spec expired_credentials(Expiration :: calendar:datetime()) -> boolean(). %% @doc Indicates if the date that is passed in has expired. %% end -expired_credentials(undefined) -> false; +expired_credentials(undefined) -> + false; expired_credentials(Expiration) -> - Now = calendar:datetime_to_gregorian_seconds(local_time()), - Expires = calendar:datetime_to_gregorian_seconds(Expiration), - Now >= Expires. - + Now = calendar:datetime_to_gregorian_seconds(local_time()), + Expires = calendar:datetime_to_gregorian_seconds(Expiration), + Now >= Expires. -spec load_credentials(State :: state()) -> {ok, state()} | {error, state()}. %% @doc Load the credentials using the following order of configuration precedence: @@ -325,138 +330,188 @@ expired_credentials(Expiration) -> %% - EC2 Instance Metadata Service %% @end load_credentials(#state{region = Region}) -> - case rabbitmq_aws_config:credentials() of - {ok, AccessKey, SecretAccessKey, Expiration, SecurityToken} -> - {ok, #state{region = Region, - error = undefined, - access_key = AccessKey, - secret_access_key = SecretAccessKey, - expiration = Expiration, - security_token = SecurityToken, - imdsv2_token = undefined}}; - {error, Reason} -> - ?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, - access_key = undefined, - secret_access_key = undefined, - expiration = undefined, - security_token = undefined, - imdsv2_token = undefined}} - end. - + case rabbitmq_aws_config:credentials() of + {ok, AccessKey, SecretAccessKey, Expiration, SecurityToken} -> + {ok, #state{ + region = Region, + error = undefined, + access_key = AccessKey, + secret_access_key = SecretAccessKey, + expiration = Expiration, + security_token = SecurityToken, + imdsv2_token = undefined + }}; + {error, Reason} -> + ?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, + access_key = undefined, + secret_access_key = undefined, + expiration = undefined, + security_token = undefined, + imdsv2_token = undefined + }} + end. -spec local_time() -> calendar:datetime(). %% @doc Return the current local time. %% @end local_time() -> - [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), - Value. + [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), + Value. - --spec maybe_decode_body(ContentType :: {nonempty_string(), nonempty_string()}, Body :: body()) -> list() | body(). +-spec maybe_decode_body(ContentType :: {nonempty_string(), nonempty_string()}, Body :: body()) -> + list() | body(). %% @doc Attempt to decode the response body by its MIME %% @end 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) -> - rabbitmq_aws_json:decode(Body); + rabbitmq_aws_json:decode(Body); maybe_decode_body({_, "xml"}, Body) -> - rabbitmq_aws_xml:parse(Body); + rabbitmq_aws_xml:parse(Body); maybe_decode_body(_ContentType, Body) -> - Body. - + Body. -spec parse_content_type(ContentType :: string()) -> {Type :: string(), Subtype :: string()}. %% @doc parse a content type string returning a tuple of type/subtype %% @end parse_content_type(ContentType) -> - Parts = string:tokens(ContentType, ";"), - [Type, Subtype] = string:tokens(lists:nth(1, Parts), "/"), - {Type, Subtype}. - - --spec perform_request(State :: state(), Service :: string(), Method :: method(), - Headers :: headers(), Path :: path(), Body :: body(), - Options :: http_options(), Host :: string() | undefined) - -> {Result :: result(), NewState :: state()}. + Parts = string:tokens(ContentType, ";"), + [Type, Subtype] = string:tokens(lists:nth(1, Parts), "/"), + {Type, Subtype}. + +-spec perform_request( + State :: state(), + Service :: string(), + Method :: method(), + 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. %% @end perform_request(State, Service, Method, Headers, Path, Body, Options, Host) -> - perform_request_has_creds(has_credentials(State), State, Service, Method, - Headers, Path, Body, Options, Host). - - --spec perform_request_has_creds(HasCreds :: boolean(), State :: state(), - Service :: string(), Method :: method(), - Headers :: headers(), Path :: path(), Body :: body(), - Options :: http_options(), Host :: string() | undefined) - -> {Result :: result(), NewState :: state()}. + perform_request_has_creds( + has_credentials(State), + State, + Service, + Method, + Headers, + Path, + Body, + Options, + Host + ). + +-spec perform_request_has_creds( + HasCreds :: boolean(), + State :: state(), + Service :: string(), + Method :: method(), + 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, %% validate they have not or will not expire, performing the request if not, %% otherwise return an error result. %% @end perform_request_has_creds(true, State, Service, Method, Headers, Path, Body, Options, Host) -> - perform_request_creds_expired(expired_credentials(State#state.expiration), State, - Service, Method, Headers, Path, Body, Options, Host); + perform_request_creds_expired( + expired_credentials(State#state.expiration), + State, + Service, + Method, + Headers, + Path, + Body, + Options, + Host + ); perform_request_has_creds(false, State, _, _, _, _, _, _, _) -> - perform_request_creds_error(State). - - --spec perform_request_creds_expired(CredsExp :: boolean(), State :: state(), - Service :: string(), Method :: method(), - Headers :: headers(), Path :: path(), Body :: body(), - Options :: http_options(), Host :: string() | undefined) - -> {Result :: result(), NewState :: state()}. + perform_request_creds_error(State). + +-spec perform_request_creds_expired( + CredsExp :: boolean(), + State :: state(), + Service :: string(), + Method :: method(), + 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. %% If they haven't, perform the request, otherwise try and refresh the %% credentials before performing the request. %% @end 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_error(State#state{error = "Credentials expired!"}). - - --spec perform_request_with_creds(State :: state(), Service :: string(), Method :: method(), - Headers :: headers(), Path :: path(), Body :: body(), - Options :: http_options(), Host :: string() | undefined) - -> {Result :: result(), NewState :: state()}. + perform_request_creds_error(State#state{error = "Credentials expired!"}). + +-spec perform_request_with_creds( + State :: state(), + Service :: string(), + Method :: method(), + 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 %% expired, perform the request and return the response. %% @end perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options, Host) -> - URI = endpoint(State, Host, Service, Path), - SignedHeaders = sign_headers(State, Service, Method, URI, Headers, Body), - ContentType = proplists:get_value("content-type", SignedHeaders, undefined), - perform_request_with_creds(State, Method, URI, SignedHeaders, ContentType, Body, Options). - - --spec perform_request_with_creds(State :: state(), Method :: method(), URI :: string(), - Headers :: headers(), ContentType :: string() | undefined, - Body :: body(), Options :: http_options()) - -> {Result :: result(), NewState :: state()}. + URI = endpoint(State, Host, Service, Path), + SignedHeaders = sign_headers(State, Service, Method, URI, Headers, Body), + ContentType = proplists:get_value("content-type", SignedHeaders, undefined), + perform_request_with_creds(State, Method, URI, SignedHeaders, ContentType, Body, Options). + +-spec perform_request_with_creds( + State :: state(), + Method :: method(), + URI :: string(), + 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 %% expired, perform the request and return the response. %% @end perform_request_with_creds(State, Method, URI, Headers, undefined, "", Options0) -> - Options1 = ensure_timeout(Options0), - Response = httpc:request(Method, {URI, Headers}, Options1, []), - {format_response(Response), State}; + Options1 = ensure_timeout(Options0), + Response = httpc:request(Method, {URI, Headers}, Options1, []), + {format_response(Response), State}; perform_request_with_creds(State, Method, URI, Headers, ContentType, Body, Options0) -> - Options1 = ensure_timeout(Options0), - Response = httpc:request(Method, {URI, Headers, ContentType, Body}, Options1, []), - {format_response(Response), State}. - + Options1 = ensure_timeout(Options0), + Response = httpc:request(Method, {URI, Headers, ContentType, Body}, Options1, []), + {format_response(Response), 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 %% the request. %% @end 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 %% 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}] end. - --spec sign_headers(State :: state(), Service :: string(), Method :: method(), - URI :: string(), Headers :: headers(), Body :: body()) -> headers(). +-spec sign_headers( + State :: state(), + Service :: string(), + Method :: method(), + URI :: string(), + Headers :: headers(), + Body :: body() +) -> headers(). %% @doc Build the signed headers for the API request. %% @end -sign_headers(#state{access_key = AccessKey, - secret_access_key = SecretKey, - security_token = SecurityToken, - region = Region}, Service, Method, URI, Headers, 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}). +sign_headers( + #state{ + access_key = AccessKey, + secret_access_key = SecretKey, + security_token = SecurityToken, + region = Region + }, + Service, + Method, + URI, + Headers, + 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(). %% @doc Determine whether or not an Imdsv2Token has expired. %% @end expired_imdsv2_token(undefined) -> - ?LOG_DEBUG("EC2 IMDSv2 token has not yet been obtained"), - true; + ?LOG_DEBUG("EC2 IMDSv2 token has not yet been obtained"), + true; expired_imdsv2_token({_, _, undefined}) -> - ?LOG_DEBUG("EC2 IMDSv2 token is not available"), - true; + ?LOG_DEBUG("EC2 IMDSv2 token is not available"), + true; expired_imdsv2_token({_, _, Expiration}) -> - Now = calendar:datetime_to_gregorian_seconds(local_time()), - HasExpired = Now >= Expiration, - ?LOG_DEBUG("EC2 IMDSv2 token has expired: ~tp", [HasExpired]), - HasExpired. - + Now = calendar:datetime_to_gregorian_seconds(local_time()), + HasExpired = Now >= Expiration, + ?LOG_DEBUG("EC2 IMDSv2 token has expired: ~tp", [HasExpired]), + HasExpired. -spec ensure_imdsv2_token_valid() -> security_token(). ensure_imdsv2_token_valid() -> - Imdsv2Token = get_imdsv2_token(), - case expired_imdsv2_token(Imdsv2Token) of - true -> Value = rabbitmq_aws_config:load_imdsv2_token(), - Expiration = calendar:datetime_to_gregorian_seconds(local_time()) + ?METADATA_TOKEN_TTL_SECONDS, - set_imdsv2_token(#imdsv2token{token = Value, - expiration = Expiration}), + Imdsv2Token = get_imdsv2_token(), + case expired_imdsv2_token(Imdsv2Token) of + true -> + Value = rabbitmq_aws_config:load_imdsv2_token(), + Expiration = + calendar:datetime_to_gregorian_seconds(local_time()) + ?METADATA_TOKEN_TTL_SECONDS, + set_imdsv2_token(#imdsv2token{ + token = Value, + expiration = Expiration + }), Value; - _ -> Imdsv2Token#imdsv2token.token - end. + _ -> + Imdsv2Token#imdsv2token.token + end. -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. @@ -527,43 +602,49 @@ ensure_imdsv2_token_valid() -> %% If the credentials are not available or have expired, then refresh them before performing the request. %% @end ensure_credentials_valid() -> - ?LOG_DEBUG("Making sure AWS credentials are available and still valid"), - {ok, State} = gen_server:call(rabbitmq_aws, get_state), - case has_credentials(State) of - true -> case expired_credentials(State#state.expiration) of - true -> refresh_credentials(State); - _ -> ok + ?LOG_DEBUG("Making sure AWS credentials are available and still valid"), + {ok, State} = gen_server:call(rabbitmq_aws, get_state), + case has_credentials(State) of + true -> + case expired_credentials(State#state.expiration) of + true -> refresh_credentials(State); + _ -> ok end; - _ -> refresh_credentials(State) - end. - + _ -> + refresh_credentials(State) + end. -spec api_get_request(string(), path()) -> {'ok', list()} | {'error', term()}. %% @doc Invoke an API call to an AWS service. %% @end api_get_request(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). - + ?LOG_DEBUG("Invoking AWS request {Service: ~tp; Path: ~tp}...", [Service, Path]), + api_get_request_with_retries(Service, Path, ?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS). --spec api_get_request_with_retries(string(), path(), integer(), integer()) -> {'ok', list()} | {'error', term()}. +-spec api_get_request_with_retries(string(), path(), integer(), integer()) -> + {'ok', list()} | {'error', term()}. %% @doc Invoke an API call to an AWS service with retries. %% @end api_get_request_with_retries(_, _, 0, _) -> - ?LOG_WARNING("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]), - {error, "AWS service is unavailable"}; + ?LOG_WARNING("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]), + {error, "AWS service is unavailable"}; api_get_request_with_retries(Service, Path, Retries, WaitTimeBetweenRetries) -> - ensure_credentials_valid(), - case get(Service, Path) of - {ok, {_Headers, Payload}} -> ?LOG_DEBUG("AWS request: ~ts~nResponse: ~tp", [Path, Payload]), - {ok, Payload}; - {error, {credentials, _}} -> {error, credentials}; - {error, Message, Response} -> ?LOG_WARNING("Error occurred: ~ts", [Message]), - case Response of - {_, Payload} -> ?LOG_WARNING("Failed AWS request: ~ts~nResponse: ~tp", [Path, Payload]); - _ -> 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. + ensure_credentials_valid(), + case get(Service, Path) of + {ok, {_Headers, Payload}} -> + ?LOG_DEBUG("AWS request: ~ts~nResponse: ~tp", [Path, Payload]), + {ok, Payload}; + {error, {credentials, _}} -> + {error, credentials}; + {error, Message, Response} -> + ?LOG_WARNING("Error occurred: ~ts", [Message]), + case Response of + {_, Payload} -> + ?LOG_WARNING("Failed AWS request: ~ts~nResponse: ~tp", [Path, Payload]); + _ -> + 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. diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_app.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_app.erl index b01196ec30e1..543c8f56282d 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_app.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_app.erl @@ -16,7 +16,7 @@ %% =================================================================== start(_StartType, _StartArgs) -> - rabbitmq_aws_sup:start_link(). + rabbitmq_aws_sup:start_link(). stop(_State) -> - ok. + ok. diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_config.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_config.erl index b9c722e8f1b8..3d2ae89fe918 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_config.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_config.erl @@ -9,20 +9,22 @@ -module(rabbitmq_aws_config). %% API --export([credentials/0, - credentials/1, - value/2, - values/1, - instance_metadata_url/1, - instance_credentials_url/1, - instance_availability_zone_url/0, - instance_role_url/0, - instance_id_url/0, - instance_id/0, - load_imdsv2_token/0, - instance_metadata_request_headers/0, - region/0, - region/1]). +-export([ + credentials/0, + credentials/1, + value/2, + values/1, + instance_metadata_url/1, + instance_credentials_url/1, + instance_availability_zone_url/0, + instance_role_url/0, + instance_id_url/0, + instance_id/0, + load_imdsv2_token/0, + instance_metadata_request_headers/0, + region/0, + region/1 +]). %% Export all for unit tests -ifdef(TEST). @@ -81,7 +83,7 @@ %% will be returned. %% @end credentials() -> - credentials(profile()). + credentials(profile()). -spec credentials(string()) -> security_credentials(). %% @doc Return the credentials from environment variables, configuration or the @@ -129,10 +131,11 @@ credentials() -> %% will be returned. %% @end credentials(Profile) -> - lookup_credentials(Profile, - os:getenv("AWS_ACCESS_KEY_ID"), - os:getenv("AWS_SECRET_ACCESS_KEY")). - + lookup_credentials( + Profile, + os:getenv("AWS_ACCESS_KEY_ID"), + os:getenv("AWS_SECRET_ACCESS_KEY") + ). -spec region() -> {ok, string()}. %% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment @@ -144,8 +147,7 @@ credentials(Profile) -> %% local instance metadata server. %% @end region() -> - region(profile()). - + region(profile()). -spec region(Region :: string()) -> {ok, region()}. %% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment @@ -157,60 +159,61 @@ region() -> %% local instance metadata server. %% @end region(Profile) -> - case lookup_region(Profile, os:getenv("AWS_DEFAULT_REGION")) of - {ok, Region} -> {ok, Region}; - _ -> {ok, ?DEFAULT_REGION} - end. - + case lookup_region(Profile, os:getenv("AWS_DEFAULT_REGION")) of + {ok, Region} -> {ok, Region}; + _ -> {ok, ?DEFAULT_REGION} + end. -spec instance_id() -> {'ok', string()} | {'error', 'undefined'}. %% @doc Return the instance ID from the EC2 metadata service. %% @end instance_id() -> - URL = instance_id_url(), - parse_body_response(perform_http_get_instance_metadata(URL)). - + URL = instance_id_url(), + parse_body_response(perform_http_get_instance_metadata(URL)). --spec value(Profile :: string(), Key :: atom()) - -> Value :: any() | {error, Reason :: atom()}. +-spec value(Profile :: string(), Key :: atom()) -> + Value :: any() | {error, Reason :: atom()}. %% @doc Return the configuration data for the specified profile or an error %% if the profile is not found. %% @end value(Profile, Key) -> - get_value(Key, values(Profile)). + get_value(Key, values(Profile)). - --spec values(Profile :: string()) - -> Settings :: list() - | {error, Reason :: atom()}. +-spec values(Profile :: string()) -> + Settings :: + list() + | {error, Reason :: atom()}. %% @doc Return the configuration data for the specified profile or an error %% if the profile is not found. %% @end values(Profile) -> - case config_file_data() of - {error, Reason} -> - {error, Reason}; - Settings -> - Prefixed = lists:flatten(["profile ", Profile]), - proplists:get_value(Profile, Settings, - proplists:get_value(Prefixed, - Settings, {error, undefined})) - end. - + case config_file_data() of + {error, Reason} -> + {error, Reason}; + Settings -> + Prefixed = lists:flatten(["profile ", Profile]), + proplists:get_value( + Profile, + Settings, + proplists:get_value( + Prefixed, + Settings, + {error, undefined} + ) + ) + end. %% ----------------------------------------------------------------------------- %% Private / Internal Methods %% ----------------------------------------------------------------------------- - -spec config_file() -> string(). %% @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 %% exist. %% @end config_file() -> - config_file(os:getenv("AWS_CONFIG_FILE")). - + config_file(os:getenv("AWS_CONFIG_FILE")). -spec config_file(Path :: false | string()) -> string(). %% @doc Return the configuration file to test using either the value of the @@ -218,17 +221,15 @@ config_file() -> %% exist. %% @end config_file(false) -> - filename:join([home_path(), ".aws", "config"]); + filename:join([home_path(), ".aws", "config"]); config_file(EnvVar) -> - EnvVar. - + EnvVar. -spec config_file_data() -> list() | {error, Reason :: atom()}. %% @doc Return the values from a configuration file as a proplist by section %% @end config_file_data() -> - ini_file_data(config_file()). - + ini_file_data(config_file()). -spec credentials_file() -> string(). %% @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. %% @end 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(). %% @doc Return the shared credentials file to test using either the value of the @@ -245,25 +245,25 @@ credentials_file() -> %% is expected to exist. %% @end credentials_file(false) -> - filename:join([home_path(), ".aws", "credentials"]); + filename:join([home_path(), ".aws", "credentials"]); credentials_file(EnvVar) -> - EnvVar. + EnvVar. -spec credentials_file_data() -> list() | {error, Reason :: atom()}. %% @doc Return the values from a configuration file as a proplist by section %% @end credentials_file_data() -> - ini_file_data(credentials_file()). + ini_file_data(credentials_file()). - --spec get_value(Key :: atom(), Settings :: list()) -> any(); - (Key :: atom(), {error, Reason :: atom()}) -> {error, Reason :: atom()}. +-spec get_value + (Key :: atom(), Settings :: list()) -> any(); + (Key :: atom(), {error, Reason :: atom()}) -> {error, Reason :: atom()}. %% @doc Get the value for a key from a settings proplist. %% @end get_value(Key, Settings) when is_list(Settings) -> - proplists:get_value(Key, Settings, {error, undefined}); -get_value(_, {error, Reason}) -> {error, Reason}. - + proplists:get_value(Key, Settings, {error, undefined}); +get_value(_, {error, Reason}) -> + {error, Reason}. -spec home_path() -> string(). %% @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. %% @end home_path() -> - home_path(os:getenv("HOME")). - + home_path(os:getenv("HOME")). -spec home_path(Value :: string() | false) -> string(). %% @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(Value) -> Value. - --spec ini_file_data(Path :: string()) - -> list() | {error, atom()}. +-spec ini_file_data(Path :: string()) -> + list() | {error, atom()}. %% @doc Return the parsed ini file for the specified path. %% @end 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()) - -> list() | {error, atom()}. +-spec ini_file_data(Path :: string(), FileExists :: boolean()) -> + list() | {error, atom()}. %% @doc Return the parsed ini file for the specified path. %% @end ini_file_data(Path, true) -> - case read_file(Path) of - {ok, Lines} -> ini_parse_lines(Lines, none, none, []); - {error, Reason} -> {error, Reason} - end; -ini_file_data(_, false) -> {error, enoent}. - + case read_file(Path) of + {ok, Lines} -> ini_parse_lines(Lines, none, none, []); + {error, Reason} -> {error, Reason} + end; +ini_file_data(_, false) -> + {error, enoent}. -spec ini_format_key(any()) -> atom() | {error, type}. %% @doc Converts a ini file key to an atom, stripping any leading whitespace %% @end ini_format_key(Key) -> - case io_lib:printable_list(Key) of - true -> list_to_atom(string:strip(Key)); - false -> {error, type} - end. - + case io_lib:printable_list(Key) of + true -> list_to_atom(string:strip(Key)); + false -> {error, type} + end. --spec ini_parse_line(Section :: list(), - Key :: atom(), - Line :: binary()) - -> {Section :: list(), Key :: string() | none}. +-spec ini_parse_line( + Section :: list(), + Key :: atom(), + Line :: binary() +) -> + {Section :: list(), Key :: string() | none}. %% @doc Parse the AWS configuration INI file, returning a proplist %% @end ini_parse_line(Section, Parent, <<" ", Line/binary>>) -> - Child = proplists:get_value(Parent, Section, []), - {ok, NewChild} = ini_parse_line_parts(Child, ini_split_line(Line)), - {lists:keystore(Parent, 1, Section, {Parent, NewChild}), Parent}; + Child = proplists:get_value(Parent, Section, []), + {ok, NewChild} = ini_parse_line_parts(Child, ini_split_line(Line)), + {lists:keystore(Parent, 1, Section, {Parent, NewChild}), Parent}; ini_parse_line(Section, _, Line) -> - case ini_parse_line_parts(Section, ini_split_line(Line)) of - {ok, NewSection} -> {NewSection, none}; - {new_parent, Parent} -> {Section, Parent} - end. - + case ini_parse_line_parts(Section, ini_split_line(Line)) of + {ok, NewSection} -> {NewSection, none}; + {new_parent, Parent} -> {Section, Parent} + end. --spec ini_parse_line_parts(Section :: list(), - Parts :: list()) - -> {ok, list()} | {new_parent, atom()}. +-spec ini_parse_line_parts( + Section :: list(), + Parts :: list() +) -> + {ok, list()} | {new_parent, atom()}. %% @doc Parse the AWS configuration INI file, returning a proplist %% @end -ini_parse_line_parts(Section, []) -> {ok, Section}; +ini_parse_line_parts(Section, []) -> + {ok, Section}; ini_parse_line_parts(Section, [RawKey, Value]) -> - Key = ini_format_key(RawKey), - {ok, lists:keystore(Key, 1, Section, {Key, maybe_convert_number(Value)})}; + Key = ini_format_key(RawKey), + {ok, lists:keystore(Key, 1, Section, {Key, maybe_convert_number(Value)})}; ini_parse_line_parts(_, [RawKey]) -> - {new_parent, ini_format_key(RawKey)}. - - --spec ini_parse_lines(Lines::[binary()], - SectionName :: string() | atom(), - Parent :: atom(), - Accumulator :: list()) - -> list(). + {new_parent, ini_format_key(RawKey)}. + +-spec ini_parse_lines( + Lines :: [binary()], + SectionName :: string() | atom(), + Parent :: atom(), + Accumulator :: list() +) -> + list(). %% @doc Parse the AWS configuration INI file %% @end -ini_parse_lines([], _, _, Settings) -> Settings; -ini_parse_lines([H|T], SectionName, Parent, Settings) -> - {ok, NewSectionName} = ini_parse_section_name(SectionName, H), - {ok, NewParent, NewSettings} = ini_parse_section(H, NewSectionName, - Parent, Settings), - ini_parse_lines(T, NewSectionName, NewParent, NewSettings). - - --spec ini_parse_section(Line :: binary(), - SectionName :: string(), - Parent :: atom(), - Section :: list()) - -> {ok, NewParent :: atom(), Section :: list()}. +ini_parse_lines([], _, _, Settings) -> + Settings; +ini_parse_lines([H | T], SectionName, Parent, Settings) -> + {ok, NewSectionName} = ini_parse_section_name(SectionName, H), + {ok, NewParent, NewSettings} = ini_parse_section( + H, + NewSectionName, + Parent, + Settings + ), + ini_parse_lines(T, NewSectionName, NewParent, NewSettings). + +-spec ini_parse_section( + Line :: binary(), + SectionName :: string(), + Parent :: atom(), + Section :: list() +) -> + {ok, NewParent :: atom(), Section :: list()}. %% @doc Parse a line from the ini file, returning it as part of the appropriate %% section. %% @end ini_parse_section(Line, SectionName, Parent, Settings) -> - Section = proplists:get_value(SectionName, Settings, []), - {NewSection, NewParent} = ini_parse_line(Section, Parent, Line), - {ok, NewParent, lists:keystore(SectionName, 1, Settings, - {SectionName, NewSection})}. - - --spec ini_parse_section_name(CurrentSection :: string() | atom(), - Line :: binary()) - -> {ok, SectionName :: string()}. + Section = proplists:get_value(SectionName, Settings, []), + {NewSection, NewParent} = ini_parse_line(Section, Parent, Line), + {ok, NewParent, + lists:keystore( + SectionName, + 1, + Settings, + {SectionName, NewSection} + )}. + +-spec ini_parse_section_name( + CurrentSection :: string() | atom(), + Line :: binary() +) -> + {ok, SectionName :: string()}. %% @doc Attempts to parse a section name from the current line, returning either %% the new parsed section name, or the current section name. %% @end ini_parse_section_name(CurrentSection, Line) -> - Value = binary_to_list(Line), - case re:run(Value, "\\[([\\w\\s+\\-_]+)\\]", [{capture, all, list}]) of - {match, [_, SectionName]} -> {ok, SectionName}; - nomatch -> {ok, CurrentSection} - end. - + Value = binary_to_list(Line), + case re:run(Value, "\\[([\\w\\s+\\-_]+)\\]", [{capture, all, list}]) of + {match, [_, SectionName]} -> {ok, SectionName}; + nomatch -> {ok, CurrentSection} + end. -spec ini_split_line(binary()) -> list(). %% @doc Split a key value pair delimited by ``=`` to a list of strings. %% @end 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(). %% @doc Return the URL for querying the availability zone from the Instance %% Metadata service %% @end 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(). %% @doc Return the URL for querying temporary credentials from the Instance %% Metadata service for the specified role %% @end 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(). %% @doc Build the Instance Metadata service URL for the specified path %% @end instance_metadata_url(Path) -> - rabbitmq_aws_urilib:build(#uri{scheme = http, - authority = {undefined, ?INSTANCE_HOST, undefined}, - path = Path, query = []}). - + rabbitmq_aws_urilib:build(#uri{ + scheme = http, + authority = {undefined, ?INSTANCE_HOST, undefined}, + path = Path, + query = [] + }). -spec instance_role_url() -> string(). %% @doc Return the URL for querying the role associated with the current %% instance from the Instance Metadata service %% @end 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(). %% @doc Return the URL for obtaining EC2 IMDSv2 token from the Instance Metadata service. %% @end imdsv2_token_url() -> - instance_metadata_url(?TOKEN_URL). + instance_metadata_url(?TOKEN_URL). -spec instance_id_url() -> string(). %% @doc Return the URL for querying the id of the current instance from the Instance Metadata service. %% @end instance_id_url() -> - instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_ID], "/")). - - --spec lookup_credentials(Profile :: string(), - AccessKey :: string() | false, - SecretKey :: string() | false) - -> security_credentials(). + instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_ID], "/")). + +-spec lookup_credentials( + Profile :: string(), + AccessKey :: string() | false, + SecretKey :: string() | false +) -> + security_credentials(). %% @doc Return the access key and secret access key if they are set in %% environment variables, otherwise lookup the credentials from the config %% file for the specified profile. %% @end lookup_credentials(Profile, false, _) -> - lookup_credentials_from_config(Profile, - value(Profile, aws_access_key_id), - value(Profile, aws_secret_access_key)); + lookup_credentials_from_config( + Profile, + value(Profile, aws_access_key_id), + value(Profile, aws_secret_access_key) + ); lookup_credentials(Profile, _, false) -> - lookup_credentials_from_config(Profile, - value(Profile, aws_access_key_id), - value(Profile, aws_secret_access_key)); + lookup_credentials_from_config( + Profile, + value(Profile, aws_access_key_id), + value(Profile, aws_secret_access_key) + ); lookup_credentials(_, AccessKey, SecretKey) -> - {ok, AccessKey, SecretKey, undefined, undefined}. - - --spec lookup_credentials_from_config(Profile :: string(), - access_key() | {error, Reason :: atom()}, - secret_access_key()| {error, Reason :: atom()}) - -> security_credentials(). + {ok, AccessKey, SecretKey, undefined, undefined}. + +-spec lookup_credentials_from_config( + Profile :: string(), + access_key() | {error, Reason :: atom()}, + secret_access_key() | {error, Reason :: atom()} +) -> + security_credentials(). %% @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 %% 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 %% @end -lookup_credentials_from_config(Profile, {error,_}, _) -> - lookup_credentials_from_file(Profile, credentials_file_data()); +lookup_credentials_from_config(Profile, {error, _}, _) -> + lookup_credentials_from_file(Profile, credentials_file_data()); lookup_credentials_from_config(_, AccessKey, SecretKey) -> - {ok, AccessKey, SecretKey, undefined, undefined}. - + {ok, AccessKey, SecretKey, undefined, undefined}. --spec lookup_credentials_from_file(Profile :: string(), - Credentials :: list()) - -> security_credentials(). +-spec lookup_credentials_from_file( + Profile :: string(), + Credentials :: list() +) -> + security_credentials(). %% @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 %% get the credentials values out of it. If the file does not exist, %% attempt to lookup the values from the EC2 instance metadata service. %% @end -lookup_credentials_from_file(_, {error,_}) -> - lookup_credentials_from_instance_metadata(); +lookup_credentials_from_file(_, {error, _}) -> + lookup_credentials_from_instance_metadata(); lookup_credentials_from_file(Profile, Credentials) -> - Section = proplists:get_value(Profile, Credentials), - lookup_credentials_from_section(Section). + Section = proplists:get_value(Profile, Credentials), + lookup_credentials_from_section(Section). - --spec lookup_credentials_from_section(Credentials :: list() | undefined) - -> security_credentials(). +-spec lookup_credentials_from_section(Credentials :: list() | undefined) -> + security_credentials(). %% @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 %% profile is not set or the values are not set in the profile, attempt to %% lookup the values from the EC2 instance metadata service. %% @end lookup_credentials_from_section(undefined) -> - lookup_credentials_from_instance_metadata(); + lookup_credentials_from_instance_metadata(); lookup_credentials_from_section(Credentials) -> - AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined), - SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined), - lookup_credentials_from_proplist(AccessKey, SecretKey). - - --spec lookup_credentials_from_proplist(AccessKey :: access_key(), - SecretAccessKey :: secret_access_key()) - -> security_credentials(). + AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined), + SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined), + lookup_credentials_from_proplist(AccessKey, SecretKey). + +-spec lookup_credentials_from_proplist( + AccessKey :: access_key(), + SecretAccessKey :: secret_access_key() +) -> + security_credentials(). %% @doc Process the contents of the Credentials proplists checking if the %% access key and secret access key are both set. %% @end lookup_credentials_from_proplist(undefined, _) -> - lookup_credentials_from_instance_metadata(); + lookup_credentials_from_instance_metadata(); lookup_credentials_from_proplist(_, undefined) -> - lookup_credentials_from_instance_metadata(); + lookup_credentials_from_instance_metadata(); lookup_credentials_from_proplist(AccessKey, SecretKey) -> - {ok, AccessKey, SecretKey, undefined, undefined}. - + {ok, AccessKey, SecretKey, undefined, undefined}. --spec lookup_credentials_from_instance_metadata() - -> security_credentials(). +-spec lookup_credentials_from_instance_metadata() -> + security_credentials(). %% @spec lookup_credentials_from_instance_metadata() -> Result. %% @doc Attempt to lookup the values from the EC2 instance metadata service. %% @end lookup_credentials_from_instance_metadata() -> - Role = maybe_get_role_from_instance_metadata(), - maybe_get_credentials_from_instance_metadata(Role). - - --spec lookup_region(Profile :: string(), - Region :: false | string()) - -> {ok, string()} | {error, undefined}. + Role = maybe_get_role_from_instance_metadata(), + maybe_get_credentials_from_instance_metadata(Role). + +-spec lookup_region( + Profile :: string(), + Region :: false | string() +) -> + {ok, string()} | {error, undefined}. %% @doc If Region is false, lookup the region from the config or the EC2 %% instance metadata service. %% @end lookup_region(Profile, false) -> - lookup_region_from_config(values(Profile)); -lookup_region(_, Region) -> {ok, Region}. + lookup_region_from_config(values(Profile)); +lookup_region(_, Region) -> + {ok, Region}. - --spec lookup_region_from_config(Settings :: list() | {error, atom()}) - -> {ok, string()} | {error, undefined}. +-spec lookup_region_from_config(Settings :: list() | {error, atom()}) -> + {ok, string()} | {error, undefined}. %% @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 %% metadata service. %% @end 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_settings(proplists:get_value(region, Settings)). - + lookup_region_from_settings(proplists:get_value(region, Settings)). --spec lookup_region_from_settings(any() | undefined) - -> {ok, string()} | {error, undefined}. +-spec lookup_region_from_settings(any() | undefined) -> + {ok, string()} | {error, undefined}. %% @doc Decide if the region should be loaded from the Instance Metadata service %% of if it's already set. %% @end lookup_region_from_settings(undefined) -> - maybe_get_region_from_instance_metadata(); + maybe_get_region_from_instance_metadata(); lookup_region_from_settings(Region) -> - {ok, Region}. - + {ok, Region}. -spec maybe_convert_number(string()) -> integer() | float(). %% @doc Returns an integer or float from a string if possible, otherwise %% returns the string(). %% @end maybe_convert_number(Value) -> - Stripped = string:strip(Value), - case string:to_float(Stripped) of - {error,no_float} -> - try - list_to_integer(Stripped) - catch - error:badarg -> Stripped - end; - {F,_Rest} -> F - end. - - --spec maybe_get_credentials_from_instance_metadata({ok, Role :: string()} | - {error, undefined}) - -> {'ok', security_credentials()} | {'error', term()}. + Stripped = string:strip(Value), + case string:to_float(Stripped) of + {error, no_float} -> + try + list_to_integer(Stripped) + catch + error:badarg -> Stripped + end; + {F, _Rest} -> + F + end. + +-spec maybe_get_credentials_from_instance_metadata( + {ok, Role :: string()} + | {error, undefined} +) -> + {'ok', security_credentials()} | {'error', term()}. %% @doc Try to query the EC2 local instance metadata service to get temporary %% authentication credentials. %% @end maybe_get_credentials_from_instance_metadata({error, undefined}) -> - {error, undefined}; + {error, undefined}; maybe_get_credentials_from_instance_metadata({ok, Role}) -> - URL = instance_credentials_url(Role), - parse_credentials_response(perform_http_get_instance_metadata(URL)). - + URL = instance_credentials_url(Role), + parse_credentials_response(perform_http_get_instance_metadata(URL)). --spec maybe_get_region_from_instance_metadata() - -> {ok, Region :: string()} | {error, Reason :: atom()}. +-spec maybe_get_region_from_instance_metadata() -> + {ok, Region :: string()} | {error, Reason :: atom()}. %% @doc Try to query the EC2 local instance metadata service to get the region %% @end maybe_get_region_from_instance_metadata() -> - URL = instance_availability_zone_url(), - parse_az_response(perform_http_get_instance_metadata(URL)). - + URL = instance_availability_zone_url(), + parse_az_response(perform_http_get_instance_metadata(URL)). %% @doc Try to query the EC2 local instance metadata service to get the role %% assigned to the instance. %% @end maybe_get_role_from_instance_metadata() -> - URL = instance_role_url(), - parse_body_response(perform_http_get_instance_metadata(URL)). - + URL = instance_role_url(), + parse_body_response(perform_http_get_instance_metadata(URL)). --spec parse_az_response(httpc_result()) - -> {ok, Region :: string()} | {error, Reason :: atom()}. +-spec parse_az_response(httpc_result()) -> + {ok, Region :: string()} | {error, Reason :: atom()}. %% @doc Parse the response from the Availability Zone query to the %% Instance Metadata service, returning the Region if successful. %% end. parse_az_response({error, _}) -> {error, undefined}; -parse_az_response({ok, {{_, 200, _}, _, Body}}) - -> {ok, region_from_availability_zone(Body)}; +parse_az_response({ok, {{_, 200, _}, _, Body}}) -> {ok, region_from_availability_zone(Body)}; parse_az_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}. - --spec parse_body_response(httpc_result()) - -> {ok, Value :: string()} | {error, Reason :: atom()}. +-spec parse_body_response(httpc_result()) -> + {ok, Value :: string()} | {error, Reason :: atom()}. %% @doc Parse the return response from the Instance Metadata Service where the %% body value is the string to process. %% end. -parse_body_response({error, _}) -> {error, undefined}; -parse_body_response({ok, {{_, 200, _}, _, Body}}) -> {ok, Body}; +parse_body_response({error, _}) -> + {error, undefined}; +parse_body_response({ok, {{_, 200, _}, _, Body}}) -> + {ok, Body}; parse_body_response({ok, {{_, 401, _}, _, _}}) -> - ?LOG_ERROR(get_instruction_on_instance_metadata_error("Unauthorized instance metadata service request.")), - {error, undefined}; + ?LOG_ERROR( + get_instruction_on_instance_metadata_error( + "Unauthorized instance metadata service request." + ) + ), + {error, undefined}; 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.")), - {error, undefined}; -parse_body_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}. - + ?LOG_ERROR( + get_instruction_on_instance_metadata_error( + "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(). %% @doc Try to query the EC2 local instance metadata service to get the role %% assigned to the instance. %% @end -parse_credentials_response({error, _}) -> {error, undefined}; -parse_credentials_response({ok, {{_, 404, _}, _, _}}) -> {error, undefined}; +parse_credentials_response({error, _}) -> + {error, undefined}; +parse_credentials_response({ok, {{_, 404, _}, _, _}}) -> + {error, undefined}; parse_credentials_response({ok, {{_, 200, _}, _, Body}}) -> - Parsed = rabbitmq_aws_json:decode(Body), - {ok, - proplists:get_value("AccessKeyId", Parsed), - proplists:get_value("SecretAccessKey", Parsed), - parse_iso8601_timestamp(proplists:get_value("Expiration", Parsed)), - proplists:get_value("Token", Parsed)}. - + Parsed = rabbitmq_aws_json:decode(Body), + {ok, proplists:get_value("AccessKeyId", Parsed), proplists:get_value("SecretAccessKey", Parsed), + parse_iso8601_timestamp(proplists:get_value("Expiration", Parsed)), + proplists:get_value("Token", Parsed)}. -spec perform_http_get_instance_metadata(string()) -> httpc_result(). %% @doc Wrap httpc:get/4 to simplify Instance Metadata service v2 requests %% @end perform_http_get_instance_metadata(URL) -> - ?LOG_DEBUG("Querying instance metadata service: ~tp", [URL]), - httpc:request(get, {URL, instance_metadata_request_headers()}, - [{timeout, ?DEFAULT_HTTP_TIMEOUT}], []). + ?LOG_DEBUG("Querying instance metadata service: ~tp", [URL]), + httpc:request( + get, + {URL, instance_metadata_request_headers()}, + [{timeout, ?DEFAULT_HTTP_TIMEOUT}], + [] + ). -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. %% end get_instruction_on_instance_metadata_error(ErrorMessage) -> - ErrorMessage ++ - " 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.". - + ErrorMessage ++ + " 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.". -spec parse_iso8601_timestamp(Timestamp :: string() | binary()) -> calendar:datetime(). %% @doc Parse a ISO8601 timestamp, returning a datetime() value. %% @end 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) -> - [Date, Time] = string:tokens(Timestamp, "T"), - [Year, Month, Day] = string:tokens(Date, "-"), - [Hour, Minute, Second] = string:tokens(Time, ":"), - {{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))}}. - + [Date, Time] = string:tokens(Timestamp, "T"), + [Year, Month, Day] = string:tokens(Date, "-"), + [Hour, Minute, Second] = string:tokens(Time, ":"), + {{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)) + }}. -spec profile() -> string(). %% @doc Return the value of the AWS_DEFAULT_PROFILE environment variable or the @@ -687,7 +712,6 @@ parse_iso8601_timestamp(Timestamp) -> %% @end profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")). - -spec profile(false | string()) -> string(). %% @doc Process the value passed in to determine if we will return the default %% profile or the value from the environment variable. @@ -695,7 +719,6 @@ profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")). profile(false) -> ?DEFAULT_PROFILE; profile(Value) -> Value. - -spec read_file(string()) -> {'ok', [binary()]} | {error, Reason :: atom()}. %% @doc Read the specified file, returning the contents as a list of strings. %% @end @@ -703,54 +726,67 @@ read_file(Path) -> case file:read_file(Path) of {ok, Binary} -> {ok, re:split(Binary, <<"\r\n|\n">>, [{return, binary}])}; - {error, _} = Error -> Error + {error, _} = Error -> + Error end. -spec region_from_availability_zone(Value :: string()) -> string(). %% @doc Strip the availability zone suffix from the region. %% @end 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(). %% @doc Attempt to obtain EC2 IMDSv2 token. %% @end load_imdsv2_token() -> - TokenUrl = imdsv2_token_url(), - ?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)}]}, - [{timeout, ?DEFAULT_HTTP_TIMEOUT}], []) of - {ok, {{_, 200, _}, _, Value}} -> - ?LOG_DEBUG("Successfully obtained EC2 IMDSv2 token."), - Value; - {error, {{_, 400, _}, _, _}} -> - ?LOG_WARNING("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. - + TokenUrl = imdsv2_token_url(), + ?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)}]}, + [{timeout, ?DEFAULT_HTTP_TIMEOUT}], + [] + ) + of + {ok, {{_, 200, _}, _, Value}} -> + ?LOG_DEBUG("Successfully obtained EC2 IMDSv2 token."), + Value; + {error, {{_, 400, _}, _, _}} -> + ?LOG_WARNING( + "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(). %% @doc Return headers used for instance metadata service requests. %% @end instance_metadata_request_headers() -> - case application:get_env(rabbit, aws_prefer_imdsv2) of - {ok, false} -> []; - _ -> %% undefined or {ok, true} - ?LOG_DEBUG("EC2 Instance Metadata Service v2 (IMDSv2) is preferred."), - maybe_imdsv2_token_headers() - end. + case application:get_env(rabbit, aws_prefer_imdsv2) of + {ok, false} -> + []; + %% undefined or {ok, true} + _ -> + ?LOG_DEBUG("EC2 Instance Metadata Service v2 (IMDSv2) is preferred."), + maybe_imdsv2_token_headers() + end. -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. %% @end maybe_imdsv2_token_headers() -> - case rabbitmq_aws:ensure_imdsv2_token_valid() of - undefined -> []; - Value -> [{?METADATA_TOKEN, Value}] - end. + case rabbitmq_aws:ensure_imdsv2_token_valid() of + undefined -> []; + Value -> [{?METADATA_TOKEN, Value}] + end. diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_json.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_json.erl index 731ce3152c07..6eb994659e6b 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_json.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_json.erl @@ -13,46 +13,50 @@ %% @doc Decode a JSON string returning a proplist %% @end decode(Value) when is_list(Value) -> - decode(list_to_binary(Value)); + decode(list_to_binary(Value)); decode(<<>>) -> - []; + []; decode(Value) when is_binary(Value) -> - Decoded0 = rabbit_json:decode(Value), - Decoded = maps:to_list(Decoded0), - convert_binary_values(Decoded, []). - + Decoded0 = rabbit_json:decode(Value), + Decoded = maps:to_list(Decoded0), + convert_binary_values(Decoded, []). -spec convert_binary_values(Value :: list(), Accumulator :: list()) -> list(). %% @doc Convert the binary key/value pairs returned by rabbit_json to strings. %% @end -convert_binary_values([], Value) -> Value; -convert_binary_values([{K, V}|T], Accum) when is_map(V) -> - convert_binary_values( - T, - lists:append( - Accum, - [{binary_to_list(K), convert_binary_values(maps:to_list(V), [])}])); -convert_binary_values([{K, V}|T], Accum) when is_list(V) -> - convert_binary_values( - T, - lists:append( - Accum, - [{binary_to_list(K), convert_binary_values(V, [])}])); -convert_binary_values([{}|T],Accum) -> - convert_binary_values(T, [{} | Accum]); -convert_binary_values([{K, V}|T], Accum) when is_binary(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([M|T],Accum) when is_map(M) andalso map_size(M) =:= 0 -> - convert_binary_values(T, [{} | Accum]); -convert_binary_values([H|T], Accum) when is_map(H) -> - convert_binary_values(T, lists:append(Accum, convert_binary_values(maps:to_list(H), []))); -convert_binary_values([H|T], Accum) when is_binary(H) -> - convert_binary_values(T, lists:append(Accum, [binary_to_list(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, []))). +convert_binary_values([], Value) -> + Value; +convert_binary_values([{K, V} | T], Accum) when is_map(V) -> + convert_binary_values( + T, + lists:append( + Accum, + [{binary_to_list(K), convert_binary_values(maps:to_list(V), [])}] + ) + ); +convert_binary_values([{K, V} | T], Accum) when is_list(V) -> + convert_binary_values( + T, + lists:append( + Accum, + [{binary_to_list(K), convert_binary_values(V, [])}] + ) + ); +convert_binary_values([{} | T], Accum) -> + convert_binary_values(T, [{} | Accum]); +convert_binary_values([{K, V} | T], Accum) when is_binary(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([M | T], Accum) when is_map(M) andalso map_size(M) =:= 0 -> + convert_binary_values(T, [{} | Accum]); +convert_binary_values([H | T], Accum) when is_map(H) -> + convert_binary_values(T, lists:append(Accum, convert_binary_values(maps:to_list(H), []))); +convert_binary_values([H | T], Accum) when is_binary(H) -> + convert_binary_values(T, lists:append(Accum, [binary_to_list(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, []))). diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl index 86298d28ca8d..7a95a2b44e77 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl @@ -24,260 +24,292 @@ %% @doc Create the signed request headers %% end headers(Request) -> - RequestTimestamp = local_time(), - PayloadHash = sha256(Request#request.body), - URI = rabbitmq_aws_urilib:parse(Request#request.uri), - {_, Host, _} = URI#uri.authority, - Headers = append_headers(RequestTimestamp, - length(Request#request.body), - PayloadHash, - Host, - Request#request.security_token, - Request#request.headers), - RequestHash = request_hash(Request#request.method, - URI#uri.path, - URI#uri.query, - Headers, - Request#request.body), - AuthValue = authorization(Request#request.access_key, - Request#request.secret_access_key, - RequestTimestamp, - Request#request.region, - Request#request.service, - Headers, - RequestHash), - sort_headers(lists:merge([{"authorization", AuthValue}], Headers)). - + RequestTimestamp = local_time(), + PayloadHash = sha256(Request#request.body), + URI = rabbitmq_aws_urilib:parse(Request#request.uri), + {_, Host, _} = URI#uri.authority, + Headers = append_headers( + RequestTimestamp, + length(Request#request.body), + PayloadHash, + Host, + Request#request.security_token, + Request#request.headers + ), + RequestHash = request_hash( + Request#request.method, + URI#uri.path, + URI#uri.query, + Headers, + Request#request.body + ), + AuthValue = authorization( + Request#request.access_key, + Request#request.secret_access_key, + RequestTimestamp, + Request#request.region, + Request#request.service, + Headers, + RequestHash + ), + sort_headers(lists:merge([{"authorization", AuthValue}], Headers)). -spec amz_date(AMZTimestamp :: string()) -> string(). %% @doc Extract the date from the AMZ timestamp format. %% @end amz_date(AMZTimestamp) -> - [RequestDate, _] = string:tokens(AMZTimestamp, "T"), - RequestDate. - - --spec append_headers(AMZDate :: string(), - ContentLength :: integer(), - PayloadHash :: string(), - Hostname :: host(), - SecurityToken :: security_token(), - Headers :: headers()) -> list(). + [RequestDate, _] = string:tokens(AMZTimestamp, "T"), + RequestDate. + +-spec append_headers( + AMZDate :: string(), + ContentLength :: integer(), + PayloadHash :: string(), + Hostname :: host(), + SecurityToken :: security_token(), + Headers :: headers() +) -> list(). %% @doc Append the headers that need to be signed to the headers passed in with %% the request %% @end append_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken, Headers) -> - Defaults = default_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken), - Headers1 = [{string:to_lower(Key), Value} || {Key, Value} <- Headers], - Keys = lists:usort(lists:append([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(AccessKey :: access_key(), - SecretAccessKey :: secret_access_key(), - RequestTimestamp :: string(), - Region :: region(), - Service :: string(), - Headers :: headers(), - RequestHash :: string()) -> string(). + Defaults = default_headers(AMZDate, ContentLength, PayloadHash, Hostname, SecurityToken), + Headers1 = [{string:to_lower(Key), Value} || {Key, Value} <- Headers], + Keys = lists:usort( + lists:append( + [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( + AccessKey :: access_key(), + SecretAccessKey :: secret_access_key(), + RequestTimestamp :: string(), + Region :: region(), + Service :: string(), + Headers :: headers(), + RequestHash :: string() +) -> string(). %% @doc Return the authorization header value %% @end authorization(AccessKey, SecretAccessKey, RequestTimestamp, Region, Service, Headers, RequestHash) -> - RequestDate = amz_date(RequestTimestamp), - Scope = scope(RequestDate, Region, Service), - Credentials = ?ALGORITHM ++ " Credential=" ++ AccessKey ++ "/" ++ Scope, - SignedHeaders = "SignedHeaders=" ++ signed_headers(Headers), - StringToSign = string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash), - SigningKey = signing_key(SecretAccessKey, RequestDate, Region, Service), - Signature = string:join(["Signature", signature(StringToSign, SigningKey)], "="), - string:join([Credentials, SignedHeaders, Signature], ", "). - - --spec default_headers(RequestTimestamp :: string(), - ContentLength :: integer(), - PayloadHash :: string(), - Hostname :: host(), - SecurityToken :: security_token()) -> headers(). + RequestDate = amz_date(RequestTimestamp), + Scope = scope(RequestDate, Region, Service), + Credentials = ?ALGORITHM ++ " Credential=" ++ AccessKey ++ "/" ++ Scope, + SignedHeaders = "SignedHeaders=" ++ signed_headers(Headers), + StringToSign = string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash), + SigningKey = signing_key(SecretAccessKey, RequestDate, Region, Service), + Signature = string:join(["Signature", signature(StringToSign, SigningKey)], "="), + string:join([Credentials, SignedHeaders, Signature], ", "). + +-spec default_headers( + RequestTimestamp :: string(), + ContentLength :: integer(), + PayloadHash :: string(), + Hostname :: host(), + SecurityToken :: security_token() +) -> headers(). %% @doc build the base headers that are merged in with the headers for every %% request. %% @end default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, undefined) -> - [{"content-length", integer_to_list(ContentLength)}, - {"date", RequestTimestamp}, - {"host", Hostname}, - {"x-amz-content-sha256", PayloadHash}]; + [ + {"content-length", integer_to_list(ContentLength)}, + {"date", RequestTimestamp}, + {"host", Hostname}, + {"x-amz-content-sha256", PayloadHash} + ]; default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, SecurityToken) -> - [{"content-length", integer_to_list(ContentLength)}, - {"date", RequestTimestamp}, - {"host", Hostname}, - {"x-amz-content-sha256", PayloadHash}, - {"x-amz-security-token", SecurityToken}]. - + [ + {"content-length", integer_to_list(ContentLength)}, + {"date", RequestTimestamp}, + {"host", Hostname}, + {"x-amz-content-sha256", PayloadHash}, + {"x-amz-security-token", SecurityToken} + ]. -spec canonical_headers(Headers :: headers()) -> string(). %% @doc Convert the headers list to a line-feed delimited string in the AWZ %% canonical headers format. %% @end canonical_headers(Headers) -> - canonical_headers(sort_headers(Headers), []). + canonical_headers(sort_headers(Headers), []). -spec canonical_headers(Headers :: headers(), CanonicalHeaders :: list()) -> string(). %% @doc Convert the headers list to a line-feed delimited string in the AWZ %% canonical headers format. %% @end canonical_headers([], CanonicalHeaders) -> - lists:flatten(CanonicalHeaders); -canonical_headers([{Key, Value}|T], CanonicalHeaders) -> - Header = string:join([string:to_lower(Key), Value], ":") ++ "\n", - canonical_headers(T, lists:append(CanonicalHeaders, [Header])). - - --spec credential_scope(RequestDate :: string(), - Region :: region(), - Service :: string()) -> string(). + lists:flatten(CanonicalHeaders); +canonical_headers([{Key, Value} | T], CanonicalHeaders) -> + Header = string:join([string:to_lower(Key), Value], ":") ++ "\n", + canonical_headers(T, lists:append(CanonicalHeaders, [Header])). + +-spec credential_scope( + RequestDate :: string(), + Region :: region(), + Service :: string() +) -> string(). %% @doc Return the credential scope string used in creating the request string to sign. %% @end 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(Key :: string(), - Headers :: headers(), - Default :: string()) -> string(). +-spec header_value( + Key :: string(), + Headers :: headers(), + Default :: string() +) -> string(). %% @doc Return the the header value or the default value for the header if it %% is not specified. %% @end 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(). %% @doc Return the SHA-256 hash for the specified value. %% @end hmac_sign(Key, Message) -> - SignedValue = crypto:mac(hmac, sha256, Key, Message), - binary_to_list(SignedValue). - + SignedValue = crypto:mac(hmac, sha256, Key, Message), + binary_to_list(SignedValue). -spec local_time() -> string(). %% @doc Return the current timestamp in GMT formatted in ISO8601 basic format. %% @end local_time() -> - [LocalTime] = calendar:local_time_to_universal_time_dst(calendar:local_time()), - local_time(LocalTime). - + [LocalTime] = calendar:local_time_to_universal_time_dst(calendar:local_time()), + local_time(LocalTime). -spec local_time(calendar:datetime()) -> string(). %% @doc Return the current timestamp in GMT formatted in ISO8601 basic format. %% @end -local_time({{Y,M,D},{HH,MM,SS}}) -> - lists:flatten(io_lib:format(?ISOFORMAT_BASIC, [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])). -spec query_string(QueryArgs :: list()) -> string(). %% @doc Return the sorted query string for the specified arguments. %% @end query_string(undefined) -> ""; -query_string(QueryArgs) -> - rabbitmq_aws_urilib:build_query_string(lists:keysort(1, QueryArgs)). - - --spec request_hash(Method :: method(), - Path :: path(), - QArgs :: query_args(), - Headers :: headers(), - Payload :: string()) -> string(). +query_string(QueryArgs) -> rabbitmq_aws_urilib:build_query_string(lists:keysort(1, QueryArgs)). + +-spec request_hash( + Method :: method(), + Path :: path(), + QArgs :: query_args(), + Headers :: headers(), + Payload :: string() +) -> string(). %% @doc Create the request hash value %% @end request_hash(Method, Path, QArgs, Headers, Payload) -> - RawPath = case string:slice(Path, 0, 1) of - "/" -> Path; - _ -> "/" ++ Path - end, - EncodedPath = uri_string:recompose(#{path => RawPath}), - CanonicalRequest = string:join([string:to_upper(atom_to_list(Method)), - EncodedPath, - query_string(QArgs), - canonical_headers(Headers), - signed_headers(Headers), - sha256(Payload)], "\n"), - sha256(CanonicalRequest). - - --spec scope(AMZDate :: string(), - Region :: region(), - Service :: string()) -> string(). + RawPath = + case string:slice(Path, 0, 1) of + "/" -> Path; + _ -> "/" ++ Path + end, + EncodedPath = uri_string:recompose(#{path => RawPath}), + CanonicalRequest = string:join( + [ + string:to_upper(atom_to_list(Method)), + EncodedPath, + query_string(QArgs), + canonical_headers(Headers), + signed_headers(Headers), + sha256(Payload) + ], + "\n" + ), + sha256(CanonicalRequest). + +-spec scope( + AMZDate :: string(), + Region :: region(), + Service :: string() +) -> string(). %% @doc Create the Scope string %% @end scope(AMZDate, Region, Service) -> - string:join([AMZDate, Region, Service, "aws4_request"], "/"). - + string:join([AMZDate, Region, Service, "aws4_request"], "/"). -spec sha256(Value :: string()) -> string(). %% @doc Return the SHA-256 hash for the specified value. %% @end sha256(Value) -> - lists:flatten(io_lib:format("~64.16.0b", - [binary:decode_unsigned(crypto:hash(sha256, Value))])). - + lists:flatten( + io_lib:format( + "~64.16.0b", + [binary:decode_unsigned(crypto:hash(sha256, Value))] + ) + ). -spec signed_headers(Headers :: list()) -> string(). %% @doc Return the signed headers string of delimited header key names %% @end signed_headers(Headers) -> - signed_headers(sort_headers(Headers), []). - + signed_headers(sort_headers(Headers), []). -spec signed_headers(Headers :: headers(), Values :: list()) -> string(). %% @doc Return the signed headers string of delimited header key names %% @end -signed_headers([], SignedHeaders) -> string:join(SignedHeaders, ";"); -signed_headers([{Key,_}|T], SignedHeaders) -> - signed_headers(T, SignedHeaders ++ [string:to_lower(Key)]). - - --spec signature(StringToSign :: string(), - SigningKey :: string()) -> string(). +signed_headers([], SignedHeaders) -> + string:join(SignedHeaders, ";"); +signed_headers([{Key, _} | T], SignedHeaders) -> + signed_headers(T, SignedHeaders ++ [string:to_lower(Key)]). + +-spec signature( + StringToSign :: string(), + SigningKey :: string() +) -> string(). %% @doc Create the request signature. %% @end signature(StringToSign, SigningKey) -> - SignedValue = crypto:mac(hmac, sha256, SigningKey, StringToSign), - lists:flatten(io_lib:format("~64.16.0b", [binary:decode_unsigned(SignedValue)])). - - --spec signing_key(SecretKey :: secret_access_key(), - AMZDate :: string(), - Region :: region(), - Service :: string()) -> string(). + SignedValue = crypto:mac(hmac, sha256, SigningKey, StringToSign), + lists:flatten(io_lib:format("~64.16.0b", [binary:decode_unsigned(SignedValue)])). + +-spec signing_key( + SecretKey :: secret_access_key(), + AMZDate :: string(), + Region :: region(), + Service :: string() +) -> string(). %% @doc Create the signing key %% @end signing_key(SecretKey, AMZDate, Region, Service) -> - DateKey = hmac_sign("AWS4" ++ SecretKey, AMZDate), - RegionKey = hmac_sign(DateKey, Region), - ServiceKey = hmac_sign(RegionKey, Service), - hmac_sign(ServiceKey, "aws4_request"). - - --spec string_to_sign(RequestTimestamp :: string(), - RequestDate :: string(), - Region :: region(), - Service :: string(), - RequestHash :: string()) -> string(). + DateKey = hmac_sign("AWS4" ++ SecretKey, AMZDate), + RegionKey = hmac_sign(DateKey, Region), + ServiceKey = hmac_sign(RegionKey, Service), + hmac_sign(ServiceKey, "aws4_request"). + +-spec string_to_sign( + RequestTimestamp :: string(), + RequestDate :: string(), + Region :: region(), + Service :: string(), + RequestHash :: string() +) -> string(). %% @doc Return the string to sign when creating the signed request. %% @end string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash) -> - CredentialScope = credential_scope(RequestDate, Region, Service), - lists:flatten(string:join([ - ?ALGORITHM, - RequestTimestamp, - CredentialScope, - RequestHash - ], "\n")). - + CredentialScope = credential_scope(RequestDate, Region, Service), + lists:flatten( + string:join( + [ + ?ALGORITHM, + RequestTimestamp, + CredentialScope, + RequestHash + ], + "\n" + ) + ). -spec sort_headers(Headers :: headers()) -> headers(). %% @doc Case-insensitive sorting of the request headers %% @end 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). diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_sup.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_sup.erl index 6327b2029ddd..7c4900f7abb6 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_sup.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_sup.erl @@ -8,13 +8,15 @@ -behaviour(supervisor). --export([start_link/0, - init/1]). +-export([ + start_link/0, + init/1 +]). -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5, Type, [I]}). start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). + supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 5, 10}, [?CHILD(rabbitmq_aws, worker)]}}. + {ok, {{one_for_one, 5, 10}, [?CHILD(rabbitmq_aws, worker)]}}. diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_urilib.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_urilib.erl index d89b372f38a5..f1bc7b5a2d2c 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_urilib.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_urilib.erl @@ -7,12 +7,13 @@ %% ==================================================================== -module(rabbitmq_aws_urilib). --export([build/1, - build_query_string/1, - parse/1, - parse_userinfo/1, - parse_userinfo_result/1 - ]). +-export([ + build/1, + build_query_string/1, + parse/1, + parse_userinfo/1, + parse_userinfo_result/1 +]). %% Export all for unit tests -ifdef(TEST). @@ -25,77 +26,83 @@ %% @doc Build a URI string %% @end build(URI) -> - {UserInfo, Host, Port} = URI#uri.authority, - UriMap = #{ - scheme => to_list(URI#uri.scheme), - host => Host - }, - UriMap1 = case UserInfo of - undefined -> UriMap; - {User, undefined} -> maps:put(userinfo, User, UriMap); - {User, Password} -> maps:put(userinfo, User ++ ":" ++ Password, UriMap) - end, - UriMap2 = case Port of - undefined -> UriMap1; - Value1 -> maps:put(port, Value1, UriMap1) - end, - UriMap3 = case URI#uri.path of - undefined -> maps:put(path, "", UriMap2); - Value2 -> - PrefixedPath = case string:slice(Value2, 0, 1) of - "/" -> Value2; - _ -> "/" ++ Value2 - end, - maps:put(path, PrefixedPath, UriMap2) - end, - UriMap4 = case URI#uri.query of - undefined -> UriMap3; - "" -> UriMap3; - 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). + {UserInfo, Host, Port} = URI#uri.authority, + UriMap = #{ + scheme => to_list(URI#uri.scheme), + host => Host + }, + UriMap1 = + case UserInfo of + undefined -> UriMap; + {User, undefined} -> maps:put(userinfo, User, UriMap); + {User, Password} -> maps:put(userinfo, User ++ ":" ++ Password, UriMap) + end, + UriMap2 = + case Port of + undefined -> UriMap1; + Value1 -> maps:put(port, Value1, UriMap1) + end, + UriMap3 = + case URI#uri.path of + undefined -> + maps:put(path, "", UriMap2); + Value2 -> + PrefixedPath = + case string:slice(Value2, 0, 1) of + "/" -> Value2; + _ -> "/" ++ Value2 + end, + maps:put(path, PrefixedPath, UriMap2) + end, + UriMap4 = + case URI#uri.query of + undefined -> UriMap3; + "" -> UriMap3; + 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()}. %% @doc Parse a URI string returning a record with the parsed results %% @end parse(Value) -> - UriMap = uri_string:parse(Value), - Scheme = maps:get(scheme, UriMap, "https"), - Host = maps:get(host, UriMap), + UriMap = uri_string:parse(Value), + Scheme = maps:get(scheme, UriMap, "https"), + Host = maps:get(host, UriMap), - DefaultPort = case Scheme of - "http" -> 80; - "https" -> 443; - _ -> undefined - end, - Port = maps:get(port, UriMap, DefaultPort), - UserInfo = parse_userinfo(maps:get(userinfo, UriMap, undefined)), - Path = maps:get(path, UriMap), - Query = maps:get(query, UriMap, ""), - #uri{scheme = Scheme, - authority = {parse_userinfo(UserInfo), Host, Port}, - path = Path, - query = uri_string:dissect_query(Query), - fragment = maps:get(fragment, UriMap, undefined) - }. + DefaultPort = + case Scheme of + "http" -> 80; + "https" -> 443; + _ -> undefined + end, + Port = maps:get(port, UriMap, DefaultPort), + UserInfo = parse_userinfo(maps:get(userinfo, UriMap, undefined)), + Path = maps:get(path, UriMap), + Query = maps:get(query, UriMap, ""), + #uri{ + scheme = Scheme, + authority = {parse_userinfo(UserInfo), Host, Port}, + path = Path, + query = uri_string:dissect_query(Query), + fragment = maps:get(fragment, UriMap, undefined) + }. - --spec parse_userinfo(string() | undefined) - -> {username() | undefined, password() | undefined} | undefined. +-spec parse_userinfo(string() | undefined) -> + {username() | undefined, password() | undefined} | undefined. parse_userinfo(undefined) -> undefined; parse_userinfo([]) -> undefined; parse_userinfo({User, undefined}) -> {User, undefined}; -parse_userinfo({User, Password}) -> {User, Password}; -parse_userinfo(Value) -> - parse_userinfo_result(string:tokens(Value, ":")). - +parse_userinfo({User, Password}) -> {User, Password}; +parse_userinfo(Value) -> parse_userinfo_result(string:tokens(Value, ":")). --spec parse_userinfo_result(list()) - -> {username() | undefined, password() | undefined} | undefined. +-spec parse_userinfo_result(list()) -> + {username() | undefined, password() | undefined} | undefined. parse_userinfo_result([User, Password]) -> {User, Password}; parse_userinfo_result([User]) -> {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(). build_query_string(Args) when is_list(Args) -> - Normalized = [{to_list(K), to_list(V)} || {K, V} <- Args], - uri_string:compose_query(Normalized). + Normalized = [{to_list(K), to_list(V)} || {K, V} <- Args], + uri_string:compose_query(Normalized). -spec to_list(Val :: integer() | list() | binary() | atom() | map()) -> list(). -to_list(Val) when is_list(Val) -> 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_binary(Val) -> binary_to_list(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_atom(Val) -> atom_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). diff --git a/deps/rabbitmq_aws/src/rabbitmq_aws_xml.erl b/deps/rabbitmq_aws/src/rabbitmq_aws_xml.erl index fc3be5c642a8..250fc1fc882e 100644 --- a/deps/rabbitmq_aws/src/rabbitmq_aws_xml.erl +++ b/deps/rabbitmq_aws/src/rabbitmq_aws_xml.erl @@ -12,35 +12,32 @@ -spec parse(Value :: string() | binary()) -> list(). parse(Value) -> - {Element, _} = xmerl_scan:string(Value), - parse_node(Element). + {Element, _} = xmerl_scan:string(Value), + 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}) -> - Value = parse_content(Content, []), - [{atom_to_list(Name), flatten_value(Value, Value)}]. - - -flatten_text([], 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([H | T], Accum) when is_list(H) -> flatten_text(T, lists:append(T, Accum)). - flatten_value([L], _) when is_list(L) -> L; flatten_value(L, _) when is_list(L) -> flatten_text(L, []). - -parse_content([], Value) -> Value; +parse_content([], Value) -> + Value; parse_content(#xmlElement{} = Element, Accum) -> - lists:append(parse_node(Element), Accum); -parse_content(#xmlText{value=Value}, Accum) -> - case string:strip(Value) of - "" -> Accum; - "\n" -> Accum; - Stripped -> - lists:append([Stripped], Accum) - end; -parse_content([H|T], Accum) -> - parse_content(T, parse_content(H, Accum)). + lists:append(parse_node(Element), Accum); +parse_content(#xmlText{value = Value}, Accum) -> + case string:strip(Value) of + "" -> Accum; + "\n" -> Accum; + Stripped -> lists:append([Stripped], Accum) + end; +parse_content([H | T], Accum) -> + parse_content(T, parse_content(H, Accum)). diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_all_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_all_tests.erl index 1273e14f8bfe..ad2e497eb91f 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_all_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_all_tests.erl @@ -5,14 +5,14 @@ -include_lib("eunit/include/eunit.hrl"). run() -> - Result = { - eunit:test(rabbitmq_aws_app_tests, [verbose]), - eunit:test(rabbitmq_aws_config_tests, [verbose]), - eunit:test(rabbitmq_aws_json_tests, [verbose]), - eunit:test(rabbitmq_aws_sign_tests, [verbose]), - eunit:test(rabbitmq_aws_sup_tests, [verbose]), - eunit:test(rabbitmq_aws_tests, [verbose]), - eunit:test(rabbitmq_aws_urilib_tests, [verbose]), - eunit:test(rabbitmq_aws_xml_tests, [verbose]) - }, - ?assertEqual({ok, ok, ok, ok, ok, ok, ok, ok}, Result). + Result = { + eunit:test(rabbitmq_aws_app_tests, [verbose]), + eunit:test(rabbitmq_aws_config_tests, [verbose]), + eunit:test(rabbitmq_aws_json_tests, [verbose]), + eunit:test(rabbitmq_aws_sign_tests, [verbose]), + eunit:test(rabbitmq_aws_sup_tests, [verbose]), + eunit:test(rabbitmq_aws_tests, [verbose]), + eunit:test(rabbitmq_aws_urilib_tests, [verbose]), + eunit:test(rabbitmq_aws_xml_tests, [verbose]) + }, + ?assertEqual({ok, ok, ok, ok, ok, ok, ok, ok}, Result). diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_app_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_app_tests.erl index ced4c0065b4d..ccb95aa52738 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_app_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_app_tests.erl @@ -3,22 +3,23 @@ -include_lib("eunit/include/eunit.hrl"). start_test_() -> - {foreach, - fun() -> - meck:new(rabbitmq_aws_sup, [passthrough]) - end, - fun(_) -> - meck:unload(rabbitmq_aws_sup) - end, - [ - {"supervisor initialized", fun() -> - meck:expect(rabbitmq_aws_sup, start_link, fun() -> {ok, test_result} end), - ?assertEqual({ok, test_result}, - rabbitmq_aws_app:start(temporary, [])), - meck:validate(rabbitmq_aws_sup) - end} - ] - }. + {foreach, + fun() -> + meck:new(rabbitmq_aws_sup, [passthrough]) + end, + fun(_) -> + meck:unload(rabbitmq_aws_sup) + end, + [ + {"supervisor initialized", fun() -> + meck:expect(rabbitmq_aws_sup, start_link, fun() -> {ok, test_result} end), + ?assertEqual( + {ok, test_result}, + rabbitmq_aws_app:start(temporary, []) + ), + meck:validate(rabbitmq_aws_sup) + end} + ]}. stop_test() -> - ?assertEqual(ok, rabbitmq_aws_app:stop({})). + ?assertEqual(ok, rabbitmq_aws_app:stop({})). diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_config_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_config_tests.erl index c8329f280c07..cca1b4af8231 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_config_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_config_tests.erl @@ -4,442 +4,535 @@ -include("rabbitmq_aws.hrl"). - config_file_test_() -> - [ - {"from environment variable", fun() -> - os:putenv("AWS_CONFIG_FILE", "/etc/aws/config"), - ?assertEqual("/etc/aws/config", rabbitmq_aws_config:config_file()) - end}, - {"default without environment variable", fun() -> - os:unsetenv("AWS_CONFIG_FILE"), - os:putenv("HOME", "/home/rrabbit"), - ?assertEqual("/home/rrabbit/.aws/config", - rabbitmq_aws_config:config_file()) - end} - ]. + [ + {"from environment variable", fun() -> + os:putenv("AWS_CONFIG_FILE", "/etc/aws/config"), + ?assertEqual("/etc/aws/config", rabbitmq_aws_config:config_file()) + end}, + {"default without environment variable", fun() -> + os:unsetenv("AWS_CONFIG_FILE"), + os:putenv("HOME", "/home/rrabbit"), + ?assertEqual( + "/home/rrabbit/.aws/config", + rabbitmq_aws_config:config_file() + ) + end} + ]. config_file_data_test_() -> - [ - {"successfully parses ini", fun() -> - setup_test_config_env_var(), - Expectation = [ - {"default", - [{aws_access_key_id, "default-key"}, - {aws_secret_access_key, "default-access-key"}, - {region, "us-east-4"}]}, - {"profile testing", - [{aws_access_key_id, "foo1"}, - {aws_secret_access_key, "bar2"}, - {s3, [{max_concurrent_requests, 10}, - {max_queue_size, 1000}]}, - {region, "us-west-5"}]}, - {"profile no-region", - [{aws_access_key_id, "foo2"}, - {aws_secret_access_key, "bar3"}]}, - {"profile only-key", - [{aws_access_key_id, "foo3"}]}, - {"profile only-secret", - [{aws_secret_access_key, "foo4"}]}, - {"profile bad-entry", - [{aws_secret_access, "foo5"}]} - ], - ?assertEqual(Expectation, - rabbitmq_aws_config:config_file_data()) - end}, - {"file does not exist", fun() -> - ?assertEqual({error, enoent}, - rabbitmq_aws_config:ini_file_data(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 - } - ]. - + [ + {"successfully parses ini", fun() -> + setup_test_config_env_var(), + Expectation = [ + {"default", [ + {aws_access_key_id, "default-key"}, + {aws_secret_access_key, "default-access-key"}, + {region, "us-east-4"} + ]}, + {"profile testing", [ + {aws_access_key_id, "foo1"}, + {aws_secret_access_key, "bar2"}, + {s3, [ + {max_concurrent_requests, 10}, + {max_queue_size, 1000} + ]}, + {region, "us-west-5"} + ]}, + {"profile no-region", [ + {aws_access_key_id, "foo2"}, + {aws_secret_access_key, "bar3"} + ]}, + {"profile only-key", [{aws_access_key_id, "foo3"}]}, + {"profile only-secret", [{aws_secret_access_key, "foo4"}]}, + {"profile bad-entry", [{aws_secret_access, "foo5"}]} + ], + ?assertEqual( + Expectation, + rabbitmq_aws_config:config_file_data() + ) + end}, + {"file does not exist", fun() -> + ?assertEqual( + {error, enoent}, + rabbitmq_aws_config:ini_file_data( + 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 role URL", fun() -> - ?assertEqual("http://169.254.169.254/latest/meta-data/iam/security-credentials", - rabbitmq_aws_config:instance_role_url()) - end}, - {"availability zone URL", fun() -> - ?assertEqual("http://169.254.169.254/latest/meta-data/placement/availability-zone", - rabbitmq_aws_config:instance_availability_zone_url()) - end}, - {"instance id URL", fun() -> - ?assertEqual("http://169.254.169.254/latest/meta-data/instance-id", - 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} - ]. + [ + {"instance role URL", fun() -> + ?assertEqual( + "http://169.254.169.254/latest/meta-data/iam/security-credentials", + rabbitmq_aws_config:instance_role_url() + ) + end}, + {"availability zone URL", fun() -> + ?assertEqual( + "http://169.254.169.254/latest/meta-data/placement/availability-zone", + rabbitmq_aws_config:instance_availability_zone_url() + ) + end}, + {"instance id URL", fun() -> + ?assertEqual( + "http://169.254.169.254/latest/meta-data/instance-id", + 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_() -> - [ - {"from environment variable", fun() -> - os:putenv("AWS_SHARED_CREDENTIALS_FILE", "/etc/aws/credentials"), - ?assertEqual("/etc/aws/credentials", rabbitmq_aws_config:credentials_file()) - end}, - {"default without environment variable", fun() -> - os:unsetenv("AWS_SHARED_CREDENTIALS_FILE"), - os:putenv("HOME", "/home/rrabbit"), - ?assertEqual("/home/rrabbit/.aws/credentials", - rabbitmq_aws_config:credentials_file()) - end} - ]. - - -credentials_test_() -> - { - foreach, - fun () -> - meck:new(httpc), - meck:new(rabbitmq_aws), - reset_environment(), - [httpc, rabbitmq_aws] - end, - fun meck:unload/1, [ - {"from environment variables", fun() -> - os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), - os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), - ?assertEqual({ok, "Sésame", "ouvre-toi", undefined, undefined}, - rabbitmq_aws_config:credentials()) - end}, - {"from config file with default profile", fun() -> - setup_test_config_env_var(), - ?assertEqual({ok, "default-key", "default-access-key", undefined, undefined}, - rabbitmq_aws_config:credentials()) - end}, - {"with missing environment variable", fun() -> - os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), - meck:sequence(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "secret_imdsv2_token"), - ?assertEqual({error, undefined}, - rabbitmq_aws_config:credentials()) - end}, - {"from config file with default profile", fun() -> - setup_test_config_env_var(), - ?assertEqual({ok, "default-key", "default-access-key", undefined, undefined}, - rabbitmq_aws_config:credentials()) - end}, - {"from config file with profile", fun() -> - setup_test_config_env_var(), - ?assertEqual({ok, "foo1", "bar2", undefined, undefined}, - rabbitmq_aws_config:credentials("testing")) - end}, - {"from config file with bad profile", fun() -> - setup_test_config_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 default profile", fun() -> - setup_test_credentials_env_var(), - - ?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 - } - - ]}. + {"from environment variable", fun() -> + os:putenv("AWS_SHARED_CREDENTIALS_FILE", "/etc/aws/credentials"), + ?assertEqual("/etc/aws/credentials", rabbitmq_aws_config:credentials_file()) + end}, + {"default without environment variable", fun() -> + os:unsetenv("AWS_SHARED_CREDENTIALS_FILE"), + os:putenv("HOME", "/home/rrabbit"), + ?assertEqual( + "/home/rrabbit/.aws/credentials", + rabbitmq_aws_config:credentials_file() + ) + end} + ]. +credentials_test_() -> + { + foreach, + fun() -> + meck:new(httpc), + meck:new(rabbitmq_aws), + reset_environment(), + [httpc, rabbitmq_aws] + end, + fun meck:unload/1, + [ + {"from environment variables", fun() -> + os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), + os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), + ?assertEqual( + {ok, "Sésame", "ouvre-toi", undefined, undefined}, + rabbitmq_aws_config:credentials() + ) + end}, + {"from config file with default profile", fun() -> + setup_test_config_env_var(), + ?assertEqual( + {ok, "default-key", "default-access-key", undefined, undefined}, + rabbitmq_aws_config:credentials() + ) + end}, + {"with missing environment variable", fun() -> + os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), + meck:sequence(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "secret_imdsv2_token"), + ?assertEqual( + {error, undefined}, + rabbitmq_aws_config:credentials() + ) + end}, + {"from config file with default profile", fun() -> + setup_test_config_env_var(), + ?assertEqual( + {ok, "default-key", "default-access-key", undefined, undefined}, + rabbitmq_aws_config:credentials() + ) + end}, + {"from config file with profile", fun() -> + setup_test_config_env_var(), + ?assertEqual( + {ok, "foo1", "bar2", undefined, undefined}, + rabbitmq_aws_config:credentials("testing") + ) + end}, + {"from config file with bad profile", fun() -> + setup_test_config_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 default profile", fun() -> + setup_test_credentials_env_var(), + + ?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_() -> - [ - {"with HOME", fun() -> - os:putenv("HOME", "/home/rrabbit"), - ?assertEqual("/home/rrabbit", - rabbitmq_aws_config:home_path()) - end}, - {"without HOME", fun() -> - os:unsetenv("HOME"), - ?assertEqual(filename:absname("."), - rabbitmq_aws_config:home_path()) - end} - ]. - + [ + {"with HOME", fun() -> + os:putenv("HOME", "/home/rrabbit"), + ?assertEqual( + "/home/rrabbit", + rabbitmq_aws_config:home_path() + ) + end}, + {"without HOME", fun() -> + os:unsetenv("HOME"), + ?assertEqual( + filename:absname("."), + rabbitmq_aws_config:home_path() + ) + end} + ]. ini_format_key_test_() -> - [ - {"when value is list", fun() -> - ?assertEqual(test_key, rabbitmq_aws_config:ini_format_key("test_key")) - end}, - {"when value is binary", fun() -> - ?assertEqual({error, type}, rabbitmq_aws_config:ini_format_key(<<"test_key">>)) - end} - ]. - + [ + {"when value is list", fun() -> + ?assertEqual(test_key, rabbitmq_aws_config:ini_format_key("test_key")) + end}, + {"when value is binary", fun() -> + ?assertEqual({error, type}, rabbitmq_aws_config:ini_format_key(<<"test_key">>)) + end} + ]. maybe_convert_number_test_() -> - [ - {"when string contains an integer", fun() -> - ?assertEqual(123, rabbitmq_aws_config:maybe_convert_number("123")) - end}, - {"when string contains a float", fun() -> - ?assertEqual(123.456, rabbitmq_aws_config:maybe_convert_number("123.456")) - end}, - {"when string does not contain a number", fun() -> - ?assertEqual("hello, world", rabbitmq_aws_config:maybe_convert_number("hello, world")) - end} - ]. - + [ + {"when string contains an integer", fun() -> + ?assertEqual(123, rabbitmq_aws_config:maybe_convert_number("123")) + end}, + {"when string contains a float", fun() -> + ?assertEqual(123.456, rabbitmq_aws_config:maybe_convert_number("123.456")) + end}, + {"when string does not contain a number", fun() -> + ?assertEqual("hello, world", rabbitmq_aws_config:maybe_convert_number("hello, world")) + end} + ]. parse_iso8601_test_() -> - [ - {"parse test", fun() -> - Value = "2016-05-19T18:25:23Z", - Expectation = {{2016,5,19},{18,25,23}}, - ?assertEqual(Expectation, rabbitmq_aws_config:parse_iso8601_timestamp(Value)) - end} - ]. - + [ + {"parse test", fun() -> + Value = "2016-05-19T18:25:23Z", + Expectation = {{2016, 5, 19}, {18, 25, 23}}, + ?assertEqual(Expectation, rabbitmq_aws_config:parse_iso8601_timestamp(Value)) + end} + ]. profile_test_() -> - [ - {"from environment variable", fun() -> - os:putenv("AWS_DEFAULT_PROFILE", "httpc-aws test"), - ?assertEqual("httpc-aws test", rabbitmq_aws_config:profile()) - end}, - {"default without environment variable", fun() -> - os:unsetenv("AWS_DEFAULT_PROFILE"), - ?assertEqual("default", rabbitmq_aws_config:profile()) - end} - ]. - + [ + {"from environment variable", fun() -> + os:putenv("AWS_DEFAULT_PROFILE", "httpc-aws test"), + ?assertEqual("httpc-aws test", rabbitmq_aws_config:profile()) + end}, + {"default without environment variable", fun() -> + os:unsetenv("AWS_DEFAULT_PROFILE"), + ?assertEqual("default", rabbitmq_aws_config:profile()) + end} + ]. read_file_test_() -> - [ - {"file does not exist", fun() -> - ?assertEqual({error, enoent}, rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"]))) - end} - ]. - - -region_test_() -> - { - foreach, - fun () -> - meck:new(httpc), - meck:new(rabbitmq_aws), - reset_environment(), - [httpc, rabbitmq_aws] - end, - fun meck:unload/1, [ - {"with environment variable", fun() -> - os:putenv("AWS_DEFAULT_REGION", "us-west-1"), - ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) - end}, - {"with config file and specified profile", fun() -> - setup_test_config_env_var(), - ?assertEqual({ok, "us-west-5"}, rabbitmq_aws_config:region("testing")) - end}, - {"with config file using default profile", fun() -> - setup_test_config_env_var(), - ?assertEqual({ok, "us-east-4"}, rabbitmq_aws_config:region()) - end}, - {"missing profile in config", fun() -> - setup_test_config_env_var(), - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), - ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region("no-region")) - end}, - {"from instance metadata service", fun() -> - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), - meck:expect(httpc, request, 4, - {ok, {{protocol, 200, message}, headers, "us-west-1a"}}), - ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) - end}, - {"full lookup failure", fun() -> - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), - ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) - end}, - {"http error failure", 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({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) - end} - ]}. + {"file does not exist", fun() -> + ?assertEqual( + {error, enoent}, + rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"])) + ) + end} + ]. +region_test_() -> + { + foreach, + fun() -> + meck:new(httpc), + meck:new(rabbitmq_aws), + reset_environment(), + [httpc, rabbitmq_aws] + end, + fun meck:unload/1, + [ + {"with environment variable", fun() -> + os:putenv("AWS_DEFAULT_REGION", "us-west-1"), + ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) + end}, + {"with config file and specified profile", fun() -> + setup_test_config_env_var(), + ?assertEqual({ok, "us-west-5"}, rabbitmq_aws_config:region("testing")) + end}, + {"with config file using default profile", fun() -> + setup_test_config_env_var(), + ?assertEqual({ok, "us-east-4"}, rabbitmq_aws_config:region()) + end}, + {"missing profile in config", fun() -> + setup_test_config_env_var(), + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), + ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region("no-region")) + end}, + {"from instance metadata service", fun() -> + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), + meck:expect( + httpc, + request, + 4, + {ok, {{protocol, 200, message}, headers, "us-west-1a"}} + ), + ?assertEqual({ok, "us-west-1"}, rabbitmq_aws_config:region()) + end}, + {"full lookup failure", fun() -> + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), + ?assertEqual({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) + end}, + {"http error failure", 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({ok, ?DEFAULT_REGION}, rabbitmq_aws_config:region()) + end} + ] + }. instance_id_test_() -> - { - foreach, - fun () -> - meck:new(httpc), - meck:new(rabbitmq_aws), - reset_environment(), - [httpc, rabbitmq_aws] - end, - fun meck:unload/1, - [ - {"get instance id successfully", + { + foreach, fun() -> - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), - meck:expect(httpc, request, 4, {ok, {{protocol, 200, message}, headers, "instance-id"}}), - ?assertEqual({ok, "instance-id"}, rabbitmq_aws_config:instance_id()) - end - }, - {"getting instance id is rejected with invalid token error", - fun() -> - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "invalid"), - meck:expect(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 - } - ] - }. + meck:new(httpc), + meck:new(rabbitmq_aws), + reset_environment(), + [httpc, rabbitmq_aws] + end, + fun meck:unload/1, + [ + {"get instance id successfully", fun() -> + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), + meck:expect( + httpc, request, 4, {ok, {{protocol, 200, message}, headers, "instance-id"}} + ), + ?assertEqual({ok, "instance-id"}, rabbitmq_aws_config:instance_id()) + end}, + {"getting instance id is rejected with invalid token error", fun() -> + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, "invalid"), + meck:expect( + 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_() -> - { - foreach, - fun () -> - meck:new(httpc), - [httpc] - end, - fun meck:unload/1, - [ - {"fail to get imdsv2 token - timeout", + { + foreach, fun() -> - meck:expect(httpc, request, 4, {error, timeout}), - ?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token()) - end}, - {"fail to get imdsv2 token - PUT request is not valid", - fun() -> - meck:expect(httpc, request, 4, {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} - ] - }. - + meck:new(httpc), + [httpc] + end, + fun meck:unload/1, + [ + {"fail to get imdsv2 token - timeout", fun() -> + meck:expect(httpc, request, 4, {error, timeout}), + ?assertEqual(undefined, rabbitmq_aws_config:load_imdsv2_token()) + end}, + {"fail to get imdsv2 token - PUT request is not valid", fun() -> + meck:expect( + httpc, + request, + 4, + {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_() -> - { - foreach, - fun () -> - meck:new(rabbitmq_aws), - [rabbitmq_aws] - end, - fun meck:unload/1, - [ - {"imdsv2 token is not available", fun() -> - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), - ?assertEqual([], rabbitmq_aws_config:maybe_imdsv2_token_headers()) - end} - , - {"imdsv2 is available", fun() -> - IMDSv2Token = "super_secret_token_value ;)", - meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, IMDSv2Token), - ?assertEqual([{"X-aws-ec2-metadata-token", IMDSv2Token}], rabbitmq_aws_config:maybe_imdsv2_token_headers()) - end} - ] - }. + { + foreach, + fun() -> + meck:new(rabbitmq_aws), + [rabbitmq_aws] + end, + fun meck:unload/1, + [ + {"imdsv2 token is not available", fun() -> + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, undefined), + ?assertEqual([], rabbitmq_aws_config:maybe_imdsv2_token_headers()) + end}, + + {"imdsv2 is available", fun() -> + IMDSv2Token = "super_secret_token_value ;)", + meck:expect(rabbitmq_aws, ensure_imdsv2_token_valid, 0, IMDSv2Token), + ?assertEqual( + [{"X-aws-ec2-metadata-token", IMDSv2Token}], + rabbitmq_aws_config:maybe_imdsv2_token_headers() + ) + end} + ] + }. reset_environment() -> - os:unsetenv("AWS_ACCESS_KEY_ID"), - os:unsetenv("AWS_DEFAULT_REGION"), - 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_SHARED_CREDENTIALS_FILE", - "bad_credentials.ini"), - meck:expect(httpc, request, 4, {error, timeout}). + os:unsetenv("AWS_ACCESS_KEY_ID"), + os:unsetenv("AWS_DEFAULT_REGION"), + 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_SHARED_CREDENTIALS_FILE", + "bad_credentials.ini" + ), + meck:expect(httpc, request, 4, {error, timeout}). 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) -> - os:putenv(EnvVar, - filename:join([filename:absname("."), "test", - Filename])). + os:putenv( + EnvVar, + filename:join([ + filename:absname("."), + "test", + Filename + ]) + ). setup_test_credentials_env_var() -> - setup_test_file_with_env_var("AWS_SHARED_CREDENTIALS_FILE", - "test_aws_credentials.ini"). + setup_test_file_with_env_var( + "AWS_SHARED_CREDENTIALS_FILE", + "test_aws_credentials.ini" + ). diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_json_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_json_tests.erl index c69049e81efd..10d65ee2fd33 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_json_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_json_tests.erl @@ -3,69 +3,93 @@ -include_lib("eunit/include/eunit.hrl"). parse_test_() -> - [ - {"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\"}]}", - 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"}]} - ], - 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)), - ?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}, - {"binary 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\"}]}">>, - 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"}]} - ], - 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)), - ?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} - ]. + [ + {"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\"}]}", + 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"} + ]} + ], + 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) + ), + ?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}, + {"binary 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\"}]}">>, + 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"} + ]} + ], + 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) + ), + ?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} + ]. diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_sign_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_sign_tests.erl index 071c4c3ef022..fbdd0a877344 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_sign_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_sign_tests.erl @@ -3,289 +3,457 @@ -include_lib("eunit/include/eunit.hrl"). -include("rabbitmq_aws.hrl"). - amz_date_test_() -> - [ - {"value", fun() -> - ?assertEqual("20160220", - rabbitmq_aws_sign:amz_date("20160220T120000Z")) - end} - ]. - + [ + {"value", fun() -> + ?assertEqual( + "20160220", + rabbitmq_aws_sign:amz_date("20160220T120000Z") + ) + end} + ]. append_headers_test_() -> - [ - {"with security token", fun() -> - - Headers = [{"Content-Type", "application/x-amz-json-1.0"}, - {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}], - - AMZDate = "20160220T120000Z", - ContentLength = 128, - PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213", - Hostname = "ec2.amazonaws.com", - SecurityToken = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", - 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-security-token", SecurityToken}, - {"x-amz-target", "DynamoDB_20120810.DescribeTable"}], - ?assertEqual(Expectation, - rabbitmq_aws_sign:append_headers(AMZDate, ContentLength, - PayloadHash, Hostname, - SecurityToken, Headers)) - end}, - {"without security token", fun() -> - - Headers = [{"Content-Type", "application/x-amz-json-1.0"}, - {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"}], + [ + {"with security token", fun() -> + Headers = [ + {"Content-Type", "application/x-amz-json-1.0"}, + {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"} + ], - 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} - ]. + AMZDate = "20160220T120000Z", + ContentLength = 128, + PayloadHash = "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213", + Hostname = "ec2.amazonaws.com", + SecurityToken = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", + 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-security-token", SecurityToken}, + {"x-amz-target", "DynamoDB_20120810.DescribeTable"} + ], + ?assertEqual( + Expectation, + rabbitmq_aws_sign:append_headers( + AMZDate, + ContentLength, + PayloadHash, + Hostname, + SecurityToken, + Headers + ) + ) + end}, + {"without security token", fun() -> + Headers = [ + {"Content-Type", "application/x-amz-json-1.0"}, + {"X-Amz-Target", "DynamoDB_20120810.DescribeTable"} + ], + 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_() -> - [ - {"value", fun() -> - AccessKey = "AKIDEXAMPLE", - SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - RequestTimestamp = "20150830T123600Z", - Region = "us-east-1", - Service = "iam", - Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, - {"Host", "iam.amazonaws.com"}, - {"Date", "20150830T123600Z"}], - RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", - Expectation = "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;date;host, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", - ?assertEqual(Expectation, - rabbitmq_aws_sign:authorization(AccessKey, SecretKey, RequestTimestamp, - Region, Service, Headers, RequestHash)) - end} - ]. - + [ + {"value", fun() -> + AccessKey = "AKIDEXAMPLE", + SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + RequestTimestamp = "20150830T123600Z", + Region = "us-east-1", + Service = "iam", + Headers = [ + {"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, + {"Host", "iam.amazonaws.com"}, + {"Date", "20150830T123600Z"} + ], + RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", + Expectation = + "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;date;host, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", + ?assertEqual( + Expectation, + rabbitmq_aws_sign:authorization( + AccessKey, + SecretKey, + RequestTimestamp, + Region, + Service, + Headers, + RequestHash + ) + ) + end} + ]. canonical_headers_test_() -> - [ - {"with security token", fun() -> - Value = [{"Host", "iam.amazonaws.com"}, - {"Content-Type", "content-type:application/x-www-form-urlencoded; charset=utf-8"}, - {"My-Header2", "\"a b c \""}, - {"My-Header1", "a b c"}, - {"Date", "20150830T123600Z"}], - Expectation = lists:flatten([ - "content-type:content-type:application/x-www-form-urlencoded; charset=utf-8\n", - "date:20150830T123600Z\n", - "host:iam.amazonaws.com\n", - "my-header1:a b c\n", - "my-header2:\"a b c \"\n"]), - ?assertEqual(Expectation, rabbitmq_aws_sign:canonical_headers(Value)) - end} - ]. + [ + {"with security token", fun() -> + Value = [ + {"Host", "iam.amazonaws.com"}, + {"Content-Type", "content-type:application/x-www-form-urlencoded; charset=utf-8"}, + {"My-Header2", "\"a b c \""}, + {"My-Header1", "a b c"}, + {"Date", "20150830T123600Z"} + ], + Expectation = lists:flatten([ + "content-type:content-type:application/x-www-form-urlencoded; charset=utf-8\n", + "date:20150830T123600Z\n", + "host:iam.amazonaws.com\n", + "my-header1:a b c\n", + "my-header2:\"a b c \"\n" + ]), + ?assertEqual(Expectation, rabbitmq_aws_sign:canonical_headers(Value)) + end} + ]. credential_scope_test_() -> - [ - {"string value", fun() -> - RequestDate = "20150830", - Region = "us-east-1", - Service = "iam", - Expectation = "20150830/us-east-1/iam/aws4_request", - ?assertEqual(Expectation, - rabbitmq_aws_sign:credential_scope(RequestDate, Region, Service)) - end} - ]. + [ + {"string value", fun() -> + RequestDate = "20150830", + Region = "us-east-1", + Service = "iam", + Expectation = "20150830/us-east-1/iam/aws4_request", + ?assertEqual( + Expectation, + rabbitmq_aws_sign:credential_scope(RequestDate, Region, Service) + ) + end} + ]. hmac_sign_test_() -> - [ - {"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], - rabbitmq_aws_sign:hmac_sign("sixpence", "burn the witch")) - end} - ]. + [ + {"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 + ], + rabbitmq_aws_sign:hmac_sign("sixpence", "burn the witch") + ) + end} + ]. query_string_test_() -> - [ - {"properly sorted", fun() -> - QArgs = [{"Version", "2015-10-01"}, - {"Action", "RunInstances"}, - {"x-amz-algorithm", "AWS4-HMAC-SHA256"}, - {"Date", "20160220T120000Z"}, - {"x-amz-credential", "AKIDEXAMPLE/20140707/us-east-1/ec2/aws4_request"}], - 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", - ?assertEqual(Expectation, - rabbitmq_aws_sign:query_string(QArgs)) - end}, - {"undefined", fun() -> - ?assertEqual([], rabbitmq_aws_sign:query_string(undefined)) - end} - ]. + [ + {"properly sorted", fun() -> + QArgs = [ + {"Version", "2015-10-01"}, + {"Action", "RunInstances"}, + {"x-amz-algorithm", "AWS4-HMAC-SHA256"}, + {"Date", "20160220T120000Z"}, + {"x-amz-credential", "AKIDEXAMPLE/20140707/us-east-1/ec2/aws4_request"} + ], + 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", + ?assertEqual( + Expectation, + rabbitmq_aws_sign:query_string(QArgs) + ) + end}, + {"undefined", fun() -> + ?assertEqual([], rabbitmq_aws_sign:query_string(undefined)) + end} + ]. request_hash_test_() -> - [ - {"hash value", fun() -> - Method = get, - Path = "/", - QArgs = [{"Action", "ListUsers"}, {"Version", "2010-05-08"}], - Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, - {"Host", "iam.amazonaws.com"}, - {"Date", "20150830T123600Z"}], - Payload = "", - Expectation = "49b454e0f20fe17f437eaa570846fc5d687efc1752c8b5a1eeee5597a7eb92a5", - ?assertEqual(Expectation, - rabbitmq_aws_sign:request_hash(Method, Path, QArgs, Headers, Payload)) - end} - ]. + [ + {"hash value", fun() -> + Method = get, + Path = "/", + QArgs = [{"Action", "ListUsers"}, {"Version", "2010-05-08"}], + Headers = [ + {"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}, + {"Host", "iam.amazonaws.com"}, + {"Date", "20150830T123600Z"} + ], + Payload = "", + Expectation = "49b454e0f20fe17f437eaa570846fc5d687efc1752c8b5a1eeee5597a7eb92a5", + ?assertEqual( + Expectation, + rabbitmq_aws_sign:request_hash(Method, Path, QArgs, Headers, Payload) + ) + end} + ]. signature_test_() -> - [ - {"value", fun() -> - StringToSign = "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", - 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], - Expectation = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", - ?assertEqual(Expectation, rabbitmq_aws_sign:signature(StringToSign, SigningKey)) - end} - ]. - + [ + {"value", fun() -> + StringToSign = + "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", + 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 + ], + Expectation = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7", + ?assertEqual(Expectation, rabbitmq_aws_sign:signature(StringToSign, SigningKey)) + end} + ]. signed_headers_test_() -> - [ - {"with security token", fun() -> - Value = [{"X-Amz-Security-Token", "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"}, - {"Date", "20160220T120000Z"}, - {"Content-Type", "application/x-amz-json-1.0"}, - {"Host", "ec2.amazonaws.com"}, - {"Content-Length", 128}, - {"X-Amz-Content-sha256", "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213"}, - {"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} - ]. + [ + {"with security token", fun() -> + Value = [ + {"X-Amz-Security-Token", + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"}, + {"Date", "20160220T120000Z"}, + {"Content-Type", "application/x-amz-json-1.0"}, + {"Host", "ec2.amazonaws.com"}, + {"Content-Length", 128}, + {"X-Amz-Content-sha256", + "c888ac0919d062cee1d7b97f44f2a765e4dc9270bc720ba32b8d9f8720626213"}, + {"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 value", fun() -> - SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - AMZDate = "20150830", - Region = "us-east-1", - 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], - ?assertEqual(Expectation, - rabbitmq_aws_sign:signing_key(SecretKey, AMZDate, Region, Service)) - end} - ]. + [ + {"signing key value", fun() -> + SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + AMZDate = "20150830", + Region = "us-east-1", + 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 + ], + ?assertEqual( + Expectation, + rabbitmq_aws_sign:signing_key(SecretKey, AMZDate, Region, Service) + ) + end} + ]. string_to_sign_test_() -> - [ - {"string value", fun() -> - RequestTimestamp = "20150830T123600Z", - RequestDate = "20150830", - Region = "us-east-1", - Service = "iam", - RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", - Expectation = "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", - ?assertEqual(Expectation, - rabbitmq_aws_sign:string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash)) - end} - ]. + [ + {"string value", fun() -> + RequestTimestamp = "20150830T123600Z", + RequestDate = "20150830", + Region = "us-east-1", + Service = "iam", + RequestHash = "f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", + Expectation = + "AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/iam/aws4_request\nf536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59", + ?assertEqual( + Expectation, + rabbitmq_aws_sign:string_to_sign( + RequestTimestamp, RequestDate, Region, Service, RequestHash + ) + ) + end} + ]. local_time_0_test_() -> - {foreach, - fun() -> - meck:new(calendar, [passthrough, unstick]) - end, - fun(_) -> - meck:unload(calendar) - end, - [ - {"variation1", fun() -> - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 05, 08}, {12, 36, 00}}] end), - Expectation = "20150508T123600Z", - ?assertEqual(Expectation, rabbitmq_aws_sign:local_time()), - meck:validate(calendar) - end} - ]}. + {foreach, + fun() -> + meck:new(calendar, [passthrough, unstick]) + end, + fun(_) -> + meck:unload(calendar) + end, + [ + {"variation1", fun() -> + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> + [{{2015, 05, 08}, {12, 36, 00}}] + end), + Expectation = "20150508T123600Z", + ?assertEqual(Expectation, rabbitmq_aws_sign:local_time()), + meck:validate(calendar) + end} + ]}. local_time_1_test_() -> - [ - {"variation1", fun() -> - Value = {{2015, 05, 08}, {13, 15, 20}}, - Expectation = "20150508T131520Z", - ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) - end}, - {"variation2", fun() -> - Value = {{2015, 05, 08}, {06, 07, 08}}, - Expectation = "20150508T060708Z", - ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) - end} - ]. + [ + {"variation1", fun() -> + Value = {{2015, 05, 08}, {13, 15, 20}}, + Expectation = "20150508T131520Z", + ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) + end}, + {"variation2", fun() -> + Value = {{2015, 05, 08}, {06, 07, 08}}, + Expectation = "20150508T060708Z", + ?assertEqual(Expectation, rabbitmq_aws_sign:local_time(Value)) + end} + ]. headers_test_() -> - {foreach, - fun() -> - meck:new(calendar, [passthrough, unstick]) - end, - fun(_) -> - meck:unload(calendar) - end, - [ - {"without signing key", fun() -> - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 08, 30}, {12, 36, 00}}] end), - Request = #request{ - access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - service = "iam", - method = get, - region = "us-east-1", - uri = "https://iam.amazonaws.com/?Action=ListUsers&Version=2015-05-08", - body = "", - headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}]}, - Expectation = [ - {"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"}, - {"content-length", "0"}, - {"content-type", "application/x-www-form-urlencoded; charset=utf-8"}, - {"date", "20150830T123600Z"}, - {"host", "iam.amazonaws.com"}, - {"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} - ], - ?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)), - meck:validate(calendar) - end}, - {"with host header", fun() -> - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 08, 30}, {12, 36, 00}}] end), - Request = #request{ - access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - service = "iam", - method = get, - region = "us-east-1", - uri = "https://s3.us-east-1.amazonaws.com/?list-type=2", - body = "", - headers = [{"host", "gavinroy.com.s3.amazonaws.com"}]}, - Expectation = [ - {"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} - ] - }. + {foreach, + fun() -> + meck:new(calendar, [passthrough, unstick]) + end, + fun(_) -> + meck:unload(calendar) + end, + [ + {"without signing key", fun() -> + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> + [{{2015, 08, 30}, {12, 36, 00}}] + end), + Request = #request{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + service = "iam", + method = get, + region = "us-east-1", + uri = "https://iam.amazonaws.com/?Action=ListUsers&Version=2015-05-08", + body = "", + headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"}] + }, + Expectation = [ + {"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"}, + {"content-length", "0"}, + {"content-type", "application/x-www-form-urlencoded; charset=utf-8"}, + {"date", "20150830T123600Z"}, + {"host", "iam.amazonaws.com"}, + {"x-amz-content-sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} + ], + ?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)), + meck:validate(calendar) + end}, + {"with host header", fun() -> + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> + [{{2015, 08, 30}, {12, 36, 00}}] + end), + Request = #request{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + service = "iam", + method = get, + region = "us-east-1", + uri = "https://s3.us-east-1.amazonaws.com/?list-type=2", + body = "", + headers = [{"host", "gavinroy.com.s3.amazonaws.com"}] + }, + Expectation = [ + {"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} + ]}. diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_sup_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_sup_tests.erl index c26af15b381e..fdb54facb75a 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_sup_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_sup_tests.erl @@ -3,25 +3,29 @@ -include_lib("eunit/include/eunit.hrl"). start_link_test_() -> - {foreach, - fun() -> - meck:new(supervisor, [passthrough, unstick]) - end, - fun(_) -> - meck:unload(supervisor) - end, - [ - {"supervisor start_link", fun() -> - meck:expect(supervisor, start_link, fun(_, _, _) -> {ok, test_result} end), - ?assertEqual({ok, test_result}, - rabbitmq_aws_sup:start_link()), - meck:validate(supervisor) - end} - ] - }. + {foreach, + fun() -> + meck:new(supervisor, [passthrough, unstick]) + end, + fun(_) -> + meck:unload(supervisor) + end, + [ + {"supervisor start_link", fun() -> + meck:expect(supervisor, start_link, fun(_, _, _) -> {ok, test_result} end), + ?assertEqual( + {ok, test_result}, + rabbitmq_aws_sup:start_link() + ), + meck:validate(supervisor) + end} + ]}. init_test() -> - ?assertEqual({ok, {{one_for_one, 5, 10}, - [{rabbitmq_aws, {rabbitmq_aws, start_link, []}, - permanent, 5, worker, [rabbitmq_aws]}]}}, - rabbitmq_aws_sup:init([])). + ?assertEqual( + {ok, + {{one_for_one, 5, 10}, [ + {rabbitmq_aws, {rabbitmq_aws, start_link, []}, permanent, 5, worker, [rabbitmq_aws]} + ]}}, + rabbitmq_aws_sup:init([]) + ). diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl index d622d1359731..7f5eaa906e44 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl @@ -5,626 +5,750 @@ -include("rabbitmq_aws.hrl"). init_test_() -> - {foreach, - fun() -> - os:putenv("AWS_DEFAULT_REGION", "us-west-3"), - meck:new(rabbitmq_aws_config, [passthrough]) - end, - fun(_) -> - os:unsetenv("AWS_DEFAULT_REGION"), - meck:unload(rabbitmq_aws_config) - end, - [ - {"ok", fun() -> - os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), - os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-west-3"), - rabbitmq_aws:refresh_credentials(), - {ok, State} = gen_server:call(Pid, get_state), - ok = gen_server:stop(Pid), - os:unsetenv("AWS_ACCESS_KEY_ID"), - os:unsetenv("AWS_SECRET_ACCESS_KEY"), - Expectation = {state,"Sésame","ouvre-toi",undefined,undefined,"us-west-3", undefined,undefined}, - ?assertEqual(Expectation, State) - end}, - {"error", fun() -> - meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, test_result} end), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-west-3"), - rabbitmq_aws:refresh_credentials(), - {ok, State} = gen_server:call(Pid, get_state), - ok = gen_server:stop(Pid), - Expectation = {state,undefined,undefined,undefined,undefined,"us-west-3",undefined,test_result}, - ?assertEqual(Expectation, State), - meck:validate(rabbitmq_aws_config) - end} - ] - }. + {foreach, + fun() -> + os:putenv("AWS_DEFAULT_REGION", "us-west-3"), + meck:new(rabbitmq_aws_config, [passthrough]) + end, + fun(_) -> + os:unsetenv("AWS_DEFAULT_REGION"), + meck:unload(rabbitmq_aws_config) + end, + [ + {"ok", fun() -> + os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), + os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-west-3"), + rabbitmq_aws:refresh_credentials(), + {ok, State} = gen_server:call(Pid, get_state), + ok = gen_server:stop(Pid), + os:unsetenv("AWS_ACCESS_KEY_ID"), + os:unsetenv("AWS_SECRET_ACCESS_KEY"), + Expectation = + {state, "Sésame", "ouvre-toi", undefined, undefined, "us-west-3", undefined, + undefined}, + ?assertEqual(Expectation, State) + end}, + {"error", fun() -> + meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, test_result} end), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-west-3"), + rabbitmq_aws:refresh_credentials(), + {ok, State} = gen_server:call(Pid, get_state), + ok = gen_server:stop(Pid), + Expectation = + {state, undefined, undefined, undefined, undefined, "us-west-3", undefined, + test_result}, + ?assertEqual(Expectation, State), + meck:validate(rabbitmq_aws_config) + end} + ]}. terminate_test() -> - ?assertEqual(ok, rabbitmq_aws:terminate(foo, bar)). + ?assertEqual(ok, rabbitmq_aws:terminate(foo, bar)). code_change_test() -> - ?assertEqual({ok, {state, denial}}, rabbitmq_aws:code_change(foo, bar, {state, denial})). + ?assertEqual({ok, {state, denial}}, rabbitmq_aws:code_change(foo, bar, {state, denial})). endpoint_test_() -> - [ - {"specified", fun() -> - Region = "us-east-3", - Service = "dynamodb", - Path = "/", - Host = "localhost:32767", - Expectation = "https://localhost:32767/", - ?assertEqual(Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)) - end}, - {"unspecified", fun() -> - Region = "us-east-3", - Service = "dynamodb", - Path = "/", - Host = undefined, - Expectation = "https://dynamodb.us-east-3.amazonaws.com/", - ?assertEqual(Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)) - end} - ]. + [ + {"specified", fun() -> + Region = "us-east-3", + Service = "dynamodb", + Path = "/", + Host = "localhost:32767", + Expectation = "https://localhost:32767/", + ?assertEqual( + Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path) + ) + end}, + {"unspecified", fun() -> + Region = "us-east-3", + Service = "dynamodb", + Path = "/", + Host = undefined, + Expectation = "https://dynamodb.us-east-3.amazonaws.com/", + ?assertEqual( + Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path) + ) + end} + ]. endpoint_host_test_() -> - [ - {"dynamodb service", fun() -> - Expectation = "dynamodb.us-west-2.amazonaws.com", - ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("us-west-2", "dynamodb")) - end} - ]. + [ + {"dynamodb service", fun() -> + Expectation = "dynamodb.us-west-2.amazonaws.com", + ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("us-west-2", "dynamodb")) + end} + ]. cn_endpoint_host_test_() -> - [ - {"s3", fun() -> - Expectation = "s3.cn-north-1.amazonaws.com.cn", - ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-north-1", "s3")) - end}, - {"s3", fun() -> - Expectation = "s3.cn-northwest-1.amazonaws.com.cn", - ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-northwest-1", "s3")) - end} - ]. + [ + {"s3", fun() -> + Expectation = "s3.cn-north-1.amazonaws.com.cn", + ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-north-1", "s3")) + end}, + {"s3", fun() -> + Expectation = "s3.cn-northwest-1.amazonaws.com.cn", + ?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-northwest-1", "s3")) + end} + ]. expired_credentials_test_() -> - { - foreach, - fun () -> - meck:new(calendar, [passthrough, unstick]), - [calendar] - end, - fun meck:unload/1, - [ - {"true", fun() -> - Value = {{2016, 4, 1}, {12, 0, 0}}, - Expectation = true, - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2016, 4, 1}, {12, 0, 0}}] end), - ?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)), - meck:validate(calendar) - end}, - {"false", fun() -> - Value = {{2016,5, 1}, {16, 30, 0}}, - Expectation = false, - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2016, 4, 1}, {12, 0, 0}}] end), - ?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)), - meck:validate(calendar) - end}, - {"undefined", fun() -> - ?assertEqual(false, rabbitmq_aws:expired_credentials(undefined)) - end} - ] - }. + { + foreach, + fun() -> + meck:new(calendar, [passthrough, unstick]), + [calendar] + end, + fun meck:unload/1, + [ + {"true", fun() -> + Value = {{2016, 4, 1}, {12, 0, 0}}, + Expectation = true, + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> + [{{2016, 4, 1}, {12, 0, 0}}] + end), + ?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)), + meck:validate(calendar) + end}, + {"false", fun() -> + Value = {{2016, 5, 1}, {16, 30, 0}}, + Expectation = false, + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> + [{{2016, 4, 1}, {12, 0, 0}}] + end), + ?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)), + meck:validate(calendar) + end}, + {"undefined", fun() -> + ?assertEqual(false, rabbitmq_aws:expired_credentials(undefined)) + end} + ] + }. format_response_test_() -> - [ - {"ok", fun() -> - Response = {ok, {{"HTTP/1.1", 200, "Ok"}, [{"Content-Type", "text/xml"}], "Value"}}, - Expectation = {ok, {[{"Content-Type", "text/xml"}], [{"test", "Value"}]}}, - ?assertEqual(Expectation, rabbitmq_aws:format_response(Response)) - end}, - {"error", fun() -> - Response = {ok, {{"HTTP/1.1", 500, "Internal Server Error"}, [{"Content-Type", "text/xml"}], "Boom"}}, - Expectation = {error, "Internal Server Error", {[{"Content-Type", "text/xml"}], [{"error", "Boom"}]}}, - ?assertEqual(Expectation, rabbitmq_aws:format_response(Response)) - end} - ]. - + [ + {"ok", fun() -> + Response = + {ok, { + {"HTTP/1.1", 200, "Ok"}, [{"Content-Type", "text/xml"}], "Value" + }}, + Expectation = {ok, {[{"Content-Type", "text/xml"}], [{"test", "Value"}]}}, + ?assertEqual(Expectation, rabbitmq_aws:format_response(Response)) + end}, + {"error", fun() -> + Response = + {ok, { + {"HTTP/1.1", 500, "Internal Server Error"}, + [{"Content-Type", "text/xml"}], + "Boom" + }}, + Expectation = + {error, "Internal Server Error", + {[{"Content-Type", "text/xml"}], [{"error", "Boom"}]}}, + ?assertEqual(Expectation, rabbitmq_aws:format_response(Response)) + end} + ]. gen_server_call_test_() -> - { - foreach, - fun () -> - % We explicitely set a few defaults, in case the caller has - % something in ~/.aws. - os:putenv("AWS_DEFAULT_REGION", "us-west-3"), - os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), - os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), - meck:new(httpc, []), - [httpc] - end, - fun (Mods) -> - meck:unload(Mods), - os:unsetenv("AWS_DEFAULT_REGION"), - os:unsetenv("AWS_ACCESS_KEY_ID"), - os:unsetenv("AWS_SECRET_ACCESS_KEY") - end, - [ - { - "request", + { + foreach, fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-east-1"}, - Service = "ec2", - Method = get, - Headers = [], - Path = "/?Action=DescribeTags&Version=2015-10-01", - Body = "", - Options = [], - Host = undefined, - meck:expect(httpc, request, - fun(get, {"https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", _Headers}, _Options, []) -> - {ok, {{"HTTP/1.0", 200, "OK"}, [{"content-type", "application/json"}], "{\"pass\": true}"}} - end), - Expectation = {reply, {ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, State}, - Result = rabbitmq_aws:handle_call({request, Service, Method, Headers, Path, Body, Options, Host}, eunit, State), - ?assertEqual(Expectation, Result), - meck:validate(httpc) - end - }, - { - "get_state", - fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-east-1"}, - ?assertEqual({reply, {ok, State}, State}, - rabbitmq_aws:handle_call(get_state, eunit, State)) - end - }, - { - "refresh_credentials", - fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-east-1"}, - State2 = #state{access_key = "AKIDEXAMPLE2", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY2", - region = "us-east-1", - security_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L2", - expiration = calendar:local_time()}, - meck:new(rabbitmq_aws_config, [passthrough]), - meck:expect(rabbitmq_aws_config, credentials, - fun() -> - {ok, - State2#state.access_key, - State2#state.secret_access_key, - State2#state.expiration, - State2#state.security_token} - end), - ?assertEqual({reply, ok, State2}, rabbitmq_aws:handle_call(refresh_credentials, eunit, State)), - meck:validate(rabbitmq_aws_config), - meck:unload(rabbitmq_aws_config) - end - }, - { - "set_credentials", - fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-west-3"}, - ?assertEqual({reply, ok, State}, - rabbitmq_aws:handle_call({set_credentials, - State#state.access_key, - State#state.secret_access_key}, eunit, #state{region = "us-west-3"})) - end - }, - { - "set_region", - fun() -> - State = #state{access_key = "Sésame", - secret_access_key = "ouvre-toi", - region = "us-east-5"}, - ?assertEqual({reply, ok, State}, - rabbitmq_aws:handle_call({set_region, "us-east-5"}, eunit, #state{access_key = "Sésame", - secret_access_key = "ouvre-toi"})) - end - } - ] - }. + % We explicitely set a few defaults, in case the caller has + % something in ~/.aws. + os:putenv("AWS_DEFAULT_REGION", "us-west-3"), + os:putenv("AWS_ACCESS_KEY_ID", "Sésame"), + os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"), + meck:new(httpc, []), + [httpc] + end, + fun(Mods) -> + meck:unload(Mods), + os:unsetenv("AWS_DEFAULT_REGION"), + os:unsetenv("AWS_ACCESS_KEY_ID"), + os:unsetenv("AWS_SECRET_ACCESS_KEY") + end, + [ + { + "request", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-east-1" + }, + Service = "ec2", + Method = get, + Headers = [], + Path = "/?Action=DescribeTags&Version=2015-10-01", + Body = "", + Options = [], + Host = undefined, + meck:expect( + httpc, + request, + fun( + get, + {"https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", + _Headers}, + _Options, + [] + ) -> + {ok, { + {"HTTP/1.0", 200, "OK"}, + [{"content-type", "application/json"}], + "{\"pass\": true}" + }} + end + ), + Expectation = + {reply, {ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, + State}, + Result = rabbitmq_aws:handle_call( + {request, Service, Method, Headers, Path, Body, Options, Host}, eunit, State + ), + ?assertEqual(Expectation, Result), + meck:validate(httpc) + end + }, + { + "get_state", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-east-1" + }, + ?assertEqual( + {reply, {ok, State}, State}, + rabbitmq_aws:handle_call(get_state, eunit, State) + ) + end + }, + { + "refresh_credentials", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-east-1" + }, + State2 = #state{ + access_key = "AKIDEXAMPLE2", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY2", + region = "us-east-1", + security_token = + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L2", + expiration = calendar:local_time() + }, + meck:new(rabbitmq_aws_config, [passthrough]), + meck:expect( + rabbitmq_aws_config, + credentials, + fun() -> + {ok, State2#state.access_key, State2#state.secret_access_key, + State2#state.expiration, State2#state.security_token} + end + ), + ?assertEqual( + {reply, ok, State2}, + rabbitmq_aws:handle_call(refresh_credentials, eunit, State) + ), + meck:validate(rabbitmq_aws_config), + meck:unload(rabbitmq_aws_config) + end + }, + { + "set_credentials", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-west-3" + }, + ?assertEqual( + {reply, ok, State}, + rabbitmq_aws:handle_call( + {set_credentials, State#state.access_key, + State#state.secret_access_key}, + eunit, + #state{region = "us-west-3"} + ) + ) + end + }, + { + "set_region", + fun() -> + State = #state{ + access_key = "Sésame", + secret_access_key = "ouvre-toi", + region = "us-east-5" + }, + ?assertEqual( + {reply, ok, State}, + rabbitmq_aws:handle_call({set_region, "us-east-5"}, eunit, #state{ + access_key = "Sésame", + secret_access_key = "ouvre-toi" + }) + ) + end + } + ] + }. get_content_type_test_() -> - [ - {"from headers caps", fun() -> - Headers = [{"Content-Type", "text/xml"}], - Expectation = {"text", "xml"}, - ?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers)) - end}, - {"from headers lower", fun() -> - Headers = [{"content-type", "text/xml"}], - Expectation = {"text", "xml"}, - ?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers)) - end} - ]. + [ + {"from headers caps", fun() -> + Headers = [{"Content-Type", "text/xml"}], + Expectation = {"text", "xml"}, + ?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers)) + end}, + {"from headers lower", fun() -> + Headers = [{"content-type", "text/xml"}], + Expectation = {"text", "xml"}, + ?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers)) + end} + ]. has_credentials_test_() -> - [ - {"true", fun() -> - ?assertEqual(true, rabbitmq_aws:has_credentials(#state{access_key = "TESTVALUE1"})) - end}, - {"false", fun() -> - ?assertEqual(false, rabbitmq_aws:has_credentials(#state{error = "ERROR"})) - end} - ]. - - -local_time_test_() -> - { - foreach, - fun () -> - meck:new(calendar, [passthrough, unstick]), - [calendar] - end, - fun meck:unload/1, [ - {"value", fun() -> - Value = {{2016, 5, 1}, {12, 0, 0}}, - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end), - ?assertEqual(Value, rabbitmq_aws:local_time()), - meck:validate(calendar) - end} - ] - }. + {"true", fun() -> + ?assertEqual(true, rabbitmq_aws:has_credentials(#state{access_key = "TESTVALUE1"})) + end}, + {"false", fun() -> + ?assertEqual(false, rabbitmq_aws:has_credentials(#state{error = "ERROR"})) + end} + ]. +local_time_test_() -> + { + foreach, + fun() -> + meck:new(calendar, [passthrough, unstick]), + [calendar] + end, + fun meck:unload/1, + [ + {"value", fun() -> + Value = {{2016, 5, 1}, {12, 0, 0}}, + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end), + ?assertEqual(Value, rabbitmq_aws:local_time()), + meck:validate(calendar) + end} + ] + }. maybe_decode_body_test_() -> - [ - {"application/x-amz-json-1.0", fun() -> - ContentType = {"application", "x-amz-json-1.0"}, - Body = "{\"test\": true}", - Expectation = [{"test", true}], - ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) - end}, - {"application/json", fun() -> - ContentType = {"application", "json"}, - Body = "{\"test\": true}", - Expectation = [{"test", true}], - ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) - end}, - {"text/xml", fun() -> - ContentType = {"text", "xml"}, - Body = "value", - Expectation = [{"test", [{"node", "value"}]}], - ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) - end}, - {"text/html [unsupported]", fun() -> - ContentType = {"text", "html"}, - Body = "", - ?assertEqual(Body, rabbitmq_aws:maybe_decode_body(ContentType, Body)) - end} - ]. + [ + {"application/x-amz-json-1.0", fun() -> + ContentType = {"application", "x-amz-json-1.0"}, + Body = "{\"test\": true}", + Expectation = [{"test", true}], + ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) + end}, + {"application/json", fun() -> + ContentType = {"application", "json"}, + Body = "{\"test\": true}", + Expectation = [{"test", true}], + ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) + end}, + {"text/xml", fun() -> + ContentType = {"text", "xml"}, + Body = "value", + Expectation = [{"test", [{"node", "value"}]}], + ?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body)) + end}, + {"text/html [unsupported]", fun() -> + ContentType = {"text", "html"}, + Body = "", + ?assertEqual(Body, rabbitmq_aws:maybe_decode_body(ContentType, Body)) + end} + ]. parse_content_type_test_() -> - [ - {"application/x-amz-json-1.0", fun() -> - Expectation = {"application", "x-amz-json-1.0"}, - ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/x-amz-json-1.0")) - end}, - {"application/xml", fun() -> - Expectation = {"application", "xml"}, - ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/xml")) - end}, - {"text/xml;charset=UTF-8", fun() -> - Expectation = {"text", "xml"}, - ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("text/xml")) - end} - ]. - + [ + {"application/x-amz-json-1.0", fun() -> + Expectation = {"application", "x-amz-json-1.0"}, + ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/x-amz-json-1.0")) + end}, + {"application/xml", fun() -> + Expectation = {"application", "xml"}, + ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/xml")) + end}, + {"text/xml;charset=UTF-8", fun() -> + Expectation = {"text", "xml"}, + ?assertEqual(Expectation, rabbitmq_aws:parse_content_type("text/xml")) + end} + ]. perform_request_test_() -> - { - foreach, - fun () -> - meck:new(httpc, []), - meck:new(rabbitmq_aws_config, []), - [httpc, rabbitmq_aws_config] - end, - fun meck:unload/1, - [ - { - "has_credentials true", + { + foreach, fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-east-1"}, - Service = "ec2", - Method = get, - Headers = [], - Path = "/?Action=DescribeTags&Version=2015-10-01", - Body = "", - Options = [], - Host = undefined, - ExpectURI = "https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", - meck:expect(httpc, request, - fun(get, {URI, _Headers}, _Options, []) -> - case URI of - ExpectURI -> - {ok, {{"HTTP/1.0", 200, "OK"}, [{"content-type", "application/json"}], "{\"pass\": true}"}}; - _ -> - {ok, {{"HTTP/1.0", 400, "RequestFailure", [{"content-type", "application/json"}], "{\"pass\": false}"}}} + meck:new(httpc, []), + meck:new(rabbitmq_aws_config, []), + [httpc, rabbitmq_aws_config] + end, + fun meck:unload/1, + [ + { + "has_credentials true", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-east-1" + }, + Service = "ec2", + Method = get, + Headers = [], + Path = "/?Action=DescribeTags&Version=2015-10-01", + Body = "", + Options = [], + Host = undefined, + ExpectURI = + "https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", + meck:expect( + httpc, + request, + fun(get, {URI, _Headers}, _Options, []) -> + case URI of + ExpectURI -> + {ok, { + {"HTTP/1.0", 200, "OK"}, + [{"content-type", "application/json"}], + "{\"pass\": true}" + }}; + _ -> + {ok, + {{"HTTP/1.0", 400, "RequestFailure", + [{"content-type", "application/json"}], + "{\"pass\": false}"}}} + end end - end), - Expectation = {{ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, State}, - Result = rabbitmq_aws:perform_request(State, Service, Method, Headers, Path, Body, Options, Host), - ?assertEqual(Expectation, Result), - meck:validate(httpc) - end}, - { - "has_credentials false", - fun() -> - State = #state{region = "us-east-1"}, - Service = "ec2", - Method = get, - Headers = [], - Path = "/?Action=DescribeTags&Version=2015-10-01", - Body = "", - Options = [], - Host = undefined, - meck:expect(httpc, request, fun(get, {_URI, _Headers}, _Options, []) -> {ok, {{"HTTP/1.0", 400, "RequestFailure"}, [{"content-type", "application/json"}], "{\"pass\": false}"}} end), - Expectation = {{error, {credentials, State#state.error}}, State}, - Result = rabbitmq_aws:perform_request(State, Service, Method, Headers, Path, Body, Options, Host), - ?assertEqual(Expectation, Result), - meck:validate(httpc) - end - }, - { - "has expired credentials", - fun() -> - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - region = "us-east-1", - security_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", - expiration = {{1973, 1, 1}, {10, 20, 30}}}, - Service = "ec2", - Method = get, - Headers = [], - Path = "/?Action=DescribeTags&Version=2015-10-01", - Body = "", - Options = [], - Host = undefined, - meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, unit_test} end), - Expectation = {{error, {credentials, "Credentials expired!"}}, State#state{error = "Credentials expired!"}}, - Result = rabbitmq_aws:perform_request(State, Service, Method, Headers, Path, Body, Options, Host), - ?assertEqual(Expectation, Result), - meck:validate(rabbitmq_aws_config) - end - }, - { - "creds_error", - fun() -> - State = #state{error=unit_test}, - Expectation = {{error, {credentials, State#state.error}}, State}, - ?assertEqual(Expectation, rabbitmq_aws:perform_request_creds_error(State)) - end} - ] - }. + ), + Expectation = { + {ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, State + }, + Result = rabbitmq_aws:perform_request( + State, Service, Method, Headers, Path, Body, Options, Host + ), + ?assertEqual(Expectation, Result), + meck:validate(httpc) + end + }, + { + "has_credentials false", + fun() -> + State = #state{region = "us-east-1"}, + Service = "ec2", + Method = get, + Headers = [], + Path = "/?Action=DescribeTags&Version=2015-10-01", + Body = "", + Options = [], + Host = undefined, + meck:expect(httpc, request, fun(get, {_URI, _Headers}, _Options, []) -> + {ok, { + {"HTTP/1.0", 400, "RequestFailure"}, + [{"content-type", "application/json"}], + "{\"pass\": false}" + }} + end), + Expectation = {{error, {credentials, State#state.error}}, State}, + Result = rabbitmq_aws:perform_request( + State, Service, Method, Headers, Path, Body, Options, Host + ), + ?assertEqual(Expectation, Result), + meck:validate(httpc) + end + }, + { + "has expired credentials", + fun() -> + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + region = "us-east-1", + security_token = + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", + expiration = {{1973, 1, 1}, {10, 20, 30}} + }, + Service = "ec2", + Method = get, + Headers = [], + Path = "/?Action=DescribeTags&Version=2015-10-01", + Body = "", + Options = [], + Host = undefined, + meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, unit_test} end), + Expectation = {{error, {credentials, "Credentials expired!"}}, State#state{ + error = "Credentials expired!" + }}, + Result = rabbitmq_aws:perform_request( + State, Service, Method, Headers, Path, Body, Options, Host + ), + ?assertEqual(Expectation, Result), + meck:validate(rabbitmq_aws_config) + end + }, + { + "creds_error", + fun() -> + State = #state{error = unit_test}, + Expectation = {{error, {credentials, State#state.error}}, State}, + ?assertEqual(Expectation, rabbitmq_aws:perform_request_creds_error(State)) + end + } + ] + }. sign_headers_test_() -> - { - foreach, - fun () -> - meck:new(calendar, [passthrough, unstick]), - [calendar] - end, - fun meck:unload/1, - [ - {"with security token", fun() -> - Value = {{2016, 5, 1}, {12, 0, 0}}, - meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end), - State = #state{access_key = "AKIDEXAMPLE", - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - security_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", - region = "us-east-1"}, - Service = "ec2", - Method = get, - Headers = [], - Body = "", - URI = "http://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", - Expectation = [{"authorization", "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20160501/us-east-1/ec2/aws4_request, SignedHeaders=content-length;date;host;x-amz-content-sha256;x-amz-security-token, Signature=62d10b4897f7d05e4454b75895b5e372f6c2eb6997943cd913680822e94c6999"}, - {"content-length","0"}, - {"date","20160501T120000Z"}, {"host","ec2.us-east-1.amazonaws.com"}, - {"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, - {"x-amz-security-token", "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"}], - ?assertEqual(Expectation, rabbitmq_aws:sign_headers(State, Service, Method, URI, Headers, Body)), - meck:validate(calendar) - end} - ] - }. + { + foreach, + fun() -> + meck:new(calendar, [passthrough, unstick]), + [calendar] + end, + fun meck:unload/1, + [ + {"with security token", fun() -> + Value = {{2016, 5, 1}, {12, 0, 0}}, + meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end), + State = #state{ + access_key = "AKIDEXAMPLE", + secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + security_token = + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L", + region = "us-east-1" + }, + Service = "ec2", + Method = get, + Headers = [], + Body = "", + URI = "http://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01", + Expectation = [ + {"authorization", + "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20160501/us-east-1/ec2/aws4_request, SignedHeaders=content-length;date;host;x-amz-content-sha256;x-amz-security-token, Signature=62d10b4897f7d05e4454b75895b5e372f6c2eb6997943cd913680822e94c6999"}, + {"content-length", "0"}, + {"date", "20160501T120000Z"}, + {"host", "ec2.us-east-1.amazonaws.com"}, + {"x-amz-content-sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"x-amz-security-token", + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"} + ], + ?assertEqual( + Expectation, + rabbitmq_aws:sign_headers(State, Service, Method, URI, Headers, Body) + ), + meck:validate(calendar) + end} + ] + }. api_get_request_test_() -> - { - foreach, - fun () -> - meck:new(httpc, []), - meck:new(rabbitmq_aws_config, []), - [httpc, rabbitmq_aws_config] - end, - fun meck:unload/1, - [ - {"AWS service API request succeeded", - fun() -> - State = #state{access_key = "ExpiredKey", - secret_access_key = "ExpiredAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, - meck:expect(httpc, request, 4, {ok, {{"HTTP/1.0", 200, "OK"}, [{"content-type", "application/json"}], "{\"data\": \"value\"}"}}), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - rabbitmq_aws:set_credentials(State), - Result = rabbitmq_aws:api_get_request("AWS", "API"), - ok = gen_server:stop(Pid), - ?assertEqual({ok, [{"data","value"}]}, Result), - meck:validate(httpc) - end - }, - {"AWS service API request failed - credentials", + { + foreach, fun() -> - meck:expect(rabbitmq_aws_config, credentials, 0, {error, undefined}), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - Result = rabbitmq_aws:api_get_request("AWS", "API"), - ok = gen_server:stop(Pid), - ?assertEqual({error, credentials}, Result) - end - }, - {"AWS service API request failed - API error with persistent failure", - fun() -> - State = #state{access_key = "ExpiredKey", - secret_access_key = "ExpiredAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, - meck:expect(httpc, request, 4, {error, "network error"}), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - rabbitmq_aws:set_credentials(State), - Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1), - ok = gen_server:stop(Pid), - ?assertEqual({error, "AWS service is unavailable"}, Result), - meck:validate(httpc) - end - }, - {"AWS service API request succeeded after a transient error", - fun() -> - State = #state{access_key = "ExpiredKey", - secret_access_key = "ExpiredAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, - meck:expect(httpc, request, 4, meck:seq([ - {error, "network error"}, - {ok, {{"HTTP/1.0", 500, "OK"}, [{"content-type", "application/json"}], "{\"error\": \"server error\"}"}}, - {ok, {{"HTTP/1.0", 200, "OK"}, [{"content-type", "application/json"}], "{\"data\": \"value\"}"}} - ])), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - rabbitmq_aws:set_credentials(State), - Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1), - ok = gen_server:stop(Pid), - ?assertEqual({ok, [{"data","value"}]}, Result), - meck:validate(httpc) - end - } - ] - }. + meck:new(httpc, []), + meck:new(rabbitmq_aws_config, []), + [httpc, rabbitmq_aws_config] + end, + fun meck:unload/1, + [ + {"AWS service API request succeeded", fun() -> + State = #state{ + access_key = "ExpiredKey", + secret_access_key = "ExpiredAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, + meck:expect( + httpc, + request, + 4, + {ok, { + {"HTTP/1.0", 200, "OK"}, + [{"content-type", "application/json"}], + "{\"data\": \"value\"}" + }} + ), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + rabbitmq_aws:set_credentials(State), + Result = rabbitmq_aws:api_get_request("AWS", "API"), + ok = gen_server:stop(Pid), + ?assertEqual({ok, [{"data", "value"}]}, Result), + meck:validate(httpc) + end}, + {"AWS service API request failed - credentials", fun() -> + meck:expect(rabbitmq_aws_config, credentials, 0, {error, undefined}), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + Result = rabbitmq_aws:api_get_request("AWS", "API"), + ok = gen_server:stop(Pid), + ?assertEqual({error, credentials}, Result) + end}, + {"AWS service API request failed - API error with persistent failure", fun() -> + State = #state{ + access_key = "ExpiredKey", + secret_access_key = "ExpiredAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, + meck:expect(httpc, request, 4, {error, "network error"}), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + rabbitmq_aws:set_credentials(State), + Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1), + ok = gen_server:stop(Pid), + ?assertEqual({error, "AWS service is unavailable"}, Result), + meck:validate(httpc) + end}, + {"AWS service API request succeeded after a transient error", fun() -> + State = #state{ + access_key = "ExpiredKey", + secret_access_key = "ExpiredAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, + meck:expect( + httpc, + request, + 4, + meck:seq([ + {error, "network error"}, + {ok, { + {"HTTP/1.0", 500, "OK"}, + [{"content-type", "application/json"}], + "{\"error\": \"server error\"}" + }}, + {ok, { + {"HTTP/1.0", 200, "OK"}, + [{"content-type", "application/json"}], + "{\"data\": \"value\"}" + }} + ]) + ), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + rabbitmq_aws:set_credentials(State), + Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1), + ok = gen_server:stop(Pid), + ?assertEqual({ok, [{"data", "value"}]}, Result), + meck:validate(httpc) + end} + ] + }. ensure_credentials_valid_test_() -> - { - foreach, - fun () -> - meck:new(rabbitmq_aws_config, []), - [rabbitmq_aws_config] - end, - fun meck:unload/1, - [ - {"expired credentials are refreshed", + { + foreach, fun() -> - State = #state{access_key = "ExpiredKey", - secret_access_key = "ExpiredAccessKey", - region = "us-east-1", - expiration = {{2016, 4, 1}, {12, 0, 0}}}, - State2 = #state{access_key = "NewKey", - secret_access_key = "NewAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, + meck:new(rabbitmq_aws_config, []), + [rabbitmq_aws_config] + end, + fun meck:unload/1, + [ + {"expired credentials are refreshed", fun() -> + State = #state{ + access_key = "ExpiredKey", + secret_access_key = "ExpiredAccessKey", + region = "us-east-1", + expiration = {{2016, 4, 1}, {12, 0, 0}} + }, + State2 = #state{ + access_key = "NewKey", + secret_access_key = "NewAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, - meck:expect(rabbitmq_aws_config, credentials, - fun() -> - {ok, - State2#state.access_key, - State2#state.secret_access_key, - State2#state.expiration, - State2#state.security_token} - end), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - rabbitmq_aws:set_credentials(State), - Result = rabbitmq_aws:ensure_credentials_valid(), - Credentials = gen_server:call(Pid, get_state), - ok = gen_server:stop(Pid), - ?assertEqual(ok, Result), - ?assertEqual(Credentials, {ok, State2}), - meck:validate(rabbitmq_aws_config) + meck:expect( + rabbitmq_aws_config, + credentials, + fun() -> + {ok, State2#state.access_key, State2#state.secret_access_key, + State2#state.expiration, State2#state.security_token} + end + ), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + rabbitmq_aws:set_credentials(State), + Result = rabbitmq_aws:ensure_credentials_valid(), + Credentials = gen_server:call(Pid, get_state), + ok = gen_server:stop(Pid), + ?assertEqual(ok, Result), + ?assertEqual(Credentials, {ok, State2}), + meck:validate(rabbitmq_aws_config) + end}, + {"valid credentials are returned", fun() -> + State = #state{ + access_key = "GoodKey", + secret_access_key = "GoodAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + rabbitmq_aws:set_credentials(State), + Result = rabbitmq_aws:ensure_credentials_valid(), + Credentials = gen_server:call(Pid, get_state), + ok = gen_server:stop(Pid), + ?assertEqual(ok, Result), + ?assertEqual(Credentials, {ok, State}), + meck:validate(rabbitmq_aws_config) + end}, + {"load credentials if missing", fun() -> + State = #state{ + access_key = "GoodKey", + secret_access_key = "GoodAccessKey", + region = "us-east-1", + expiration = {{3016, 4, 1}, {12, 0, 0}} + }, + meck:expect( + rabbitmq_aws_config, + credentials, + fun() -> + {ok, State#state.access_key, State#state.secret_access_key, + State#state.expiration, State#state.security_token} + end + ), + {ok, Pid} = rabbitmq_aws:start_link(), + rabbitmq_aws:set_region("us-east-1"), + Result = rabbitmq_aws:ensure_credentials_valid(), + Credentials = gen_server:call(Pid, get_state), + ok = gen_server:stop(Pid), + ?assertEqual(ok, Result), + ?assertEqual(Credentials, {ok, State}), + meck:validate(rabbitmq_aws_config) + end} + ] + }. + +expired_imdsv2_token_test_() -> + [ + {"imdsv2 token is valid", fun() -> + [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), + Now = calendar:datetime_to_gregorian_seconds(Value), + Imdsv2Token = #imdsv2token{token = "value", expiration = Now + 100}, + ?assertEqual(false, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) end}, - {"valid credentials are returned", - fun() -> - State = #state{access_key = "GoodKey", - secret_access_key = "GoodAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - rabbitmq_aws:set_credentials(State), - Result = rabbitmq_aws:ensure_credentials_valid(), - Credentials = gen_server:call(Pid, get_state), - ok = gen_server:stop(Pid), - ?assertEqual(ok, Result), - ?assertEqual(Credentials, {ok, State}), - meck:validate(rabbitmq_aws_config) + {"imdsv2 token is expired", fun() -> + [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), + Now = calendar:datetime_to_gregorian_seconds(Value), + Imdsv2Token = #imdsv2token{token = "value", expiration = Now - 100}, + ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) end}, - {"load credentials if missing", - fun() -> - State = #state{access_key = "GoodKey", - secret_access_key = "GoodAccessKey", - region = "us-east-1", - expiration = {{3016, 4, 1}, {12, 0, 0}}}, - meck:expect(rabbitmq_aws_config, credentials, - fun() -> - {ok, - State#state.access_key, - State#state.secret_access_key, - State#state.expiration, - State#state.security_token} - end), - {ok, Pid} = rabbitmq_aws:start_link(), - rabbitmq_aws:set_region("us-east-1"), - Result = rabbitmq_aws:ensure_credentials_valid(), - Credentials = gen_server:call(Pid, get_state), - ok = gen_server:stop(Pid), - ?assertEqual(ok, Result), - ?assertEqual(Credentials, {ok, State}), - meck:validate(rabbitmq_aws_config) + {"imdsv2 token is not yet initialized", fun() -> + ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(undefined)) + end}, + {"imdsv2 token is undefined", fun() -> + Imdsv2Token = #imdsv2token{token = undefined, expiration = undefined}, + ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) end} - ] - }. - -expired_imdsv2_token_test_() -> - [ - {"imdsv2 token is valid", - fun() -> - [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), - Now = calendar:datetime_to_gregorian_seconds(Value), - Imdsv2Token = #imdsv2token{token = "value", expiration = Now + 100}, - ?assertEqual(false, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) - end - }, - {"imdsv2 token is expired", - fun() -> - [Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()), - Now = calendar:datetime_to_gregorian_seconds(Value), - Imdsv2Token = #imdsv2token{token = "value", expiration = Now - 100}, - ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) - end - }, - {"imdsv2 token is not yet initialized", - fun() -> - ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(undefined)) - end - }, - {"imdsv2 token is undefined", - fun() -> - Imdsv2Token = #imdsv2token{token = undefined, expiration = undefined}, - ?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token)) - end - } - ]. + ]. diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_urilib_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_urilib_tests.erl index c89e4554bee7..86594dab9f51 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_urilib_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_urilib_tests.erl @@ -5,150 +5,181 @@ -include("rabbitmq_aws.hrl"). build_test_() -> - [ - {"variation1", fun() -> - Expect = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", - Value = #uri{scheme = "amqp", - authority = {{"guest", "password"}, "rabbitmq", 5672}, - path = "/%2F", query = [{"heartbeat", "5"}]}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation2", fun() -> - Expect = "http://www.google.com:80/search?foo=bar#baz", - Value = #uri{scheme = http, - authority = {undefined, "www.google.com", 80}, - path = "/search", - query = [{"foo", "bar"}], - fragment = "baz"}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation3", fun() -> - Expect = "https://www.google.com/search", - Value = #uri{scheme = "https", - authority = {undefined, "www.google.com", undefined}, - path = "/search"}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation5", fun() -> - Expect = "https://www.google.com:443/search?foo=true", - Value = #uri{scheme = "https", - authority = {undefined, "www.google.com", 443}, - path = "/search", - query = [{"foo", true}]}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation6", fun() -> - Expect = "https://bar@www.google.com:443/search?foo=true", - Value = #uri{scheme = "https", - authority = {{"bar", undefined}, "www.google.com", 443}, - path = "/search", - query = [{"foo", true}]}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation7", fun() -> - Expect = "https://www.google.com:443/search?foo=true", - Value = #uri{scheme = "https", - authority = {undefined, "www.google.com", 443}, - path = "/search", - query = [{"foo", true}]}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation8", fun() -> - Expect = "https://:@www.google.com:443/search?foo=true", - Value = #uri{scheme = "https", - authority = {{"", ""}, "www.google.com", 443}, - path = "/search", - query = [{"foo", true}]}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation9", fun() -> - Expect = "https://bar:@www.google.com:443/search?foo=true#", - Value = #uri{scheme = "https", - authority={{"bar", ""}, "www.google.com", 443}, - path="/search", - query=[{"foo", true}], - fragment=""}, - Result = rabbitmq_aws_urilib:build(Value), - ?assertEqual(Expect, Result) - end}, - {"variation10", fun() -> - 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} - ]. - + [ + {"variation1", fun() -> + Expect = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", + Value = #uri{ + scheme = "amqp", + authority = {{"guest", "password"}, "rabbitmq", 5672}, + path = "/%2F", + query = [{"heartbeat", "5"}] + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation2", fun() -> + Expect = "http://www.google.com:80/search?foo=bar#baz", + Value = #uri{ + scheme = http, + authority = {undefined, "www.google.com", 80}, + path = "/search", + query = [{"foo", "bar"}], + fragment = "baz" + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation3", fun() -> + Expect = "https://www.google.com/search", + Value = #uri{ + scheme = "https", + authority = {undefined, "www.google.com", undefined}, + path = "/search" + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation5", fun() -> + Expect = "https://www.google.com:443/search?foo=true", + Value = #uri{ + scheme = "https", + authority = {undefined, "www.google.com", 443}, + path = "/search", + query = [{"foo", true}] + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation6", fun() -> + Expect = "https://bar@www.google.com:443/search?foo=true", + Value = #uri{ + scheme = "https", + authority = {{"bar", undefined}, "www.google.com", 443}, + path = "/search", + query = [{"foo", true}] + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation7", fun() -> + Expect = "https://www.google.com:443/search?foo=true", + Value = #uri{ + scheme = "https", + authority = {undefined, "www.google.com", 443}, + path = "/search", + query = [{"foo", true}] + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation8", fun() -> + Expect = "https://:@www.google.com:443/search?foo=true", + Value = #uri{ + scheme = "https", + authority = {{"", ""}, "www.google.com", 443}, + path = "/search", + query = [{"foo", true}] + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation9", fun() -> + Expect = "https://bar:@www.google.com:443/search?foo=true#", + Value = #uri{ + scheme = "https", + authority = {{"bar", ""}, "www.google.com", 443}, + path = "/search", + query = [{"foo", true}], + fragment = "" + }, + Result = rabbitmq_aws_urilib:build(Value), + ?assertEqual(Expect, Result) + end}, + {"variation10", fun() -> + 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_() -> - [ - {"basic list", fun() -> - ?assertEqual("foo=bar&baz=qux", - rabbitmq_aws_urilib:build_query_string([{"foo", "bar"}, - {"baz", "qux"}])) - end}, - {"empty list", fun() -> - ?assertEqual("", rabbitmq_aws_urilib:build_query_string([])) - end} - ]. - + [ + {"basic list", fun() -> + ?assertEqual( + "foo=bar&baz=qux", + rabbitmq_aws_urilib:build_query_string([ + {"foo", "bar"}, + {"baz", "qux"} + ]) + ) + end}, + {"empty list", fun() -> + ?assertEqual("", rabbitmq_aws_urilib:build_query_string([])) + end} + ]. parse_test_() -> - [ - {"variation1", fun() -> - URI = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", - Expect = #uri{scheme = "amqp", - authority = {{"guest", "password"}, "rabbitmq", 5672}, - path = "/%2F", - query = [{"heartbeat", "5"}], - fragment = undefined}, - ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) - end}, - {"variation2", fun() -> - URI = "http://www.google.com/search?foo=bar#baz", - Expect = #uri{scheme = "http", - authority = {undefined, "www.google.com", 80}, - path = "/search", - query = [{"foo", "bar"}], - fragment = "baz"}, - ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) - end}, - {"variation3", fun() -> - URI = "https://www.google.com/search", - Expect = #uri{scheme = "https", - authority = {undefined, "www.google.com", 443}, - path = "/search", - query = "", - fragment = undefined}, - ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) - end}, - {"variation4", fun() -> - URI = "https://www.google.com/search?foo=true", - 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} - ]. + [ + {"variation1", fun() -> + URI = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5", + Expect = #uri{ + scheme = "amqp", + authority = {{"guest", "password"}, "rabbitmq", 5672}, + path = "/%2F", + query = [{"heartbeat", "5"}], + fragment = undefined + }, + ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) + end}, + {"variation2", fun() -> + URI = "http://www.google.com/search?foo=bar#baz", + Expect = #uri{ + scheme = "http", + authority = {undefined, "www.google.com", 80}, + path = "/search", + query = [{"foo", "bar"}], + fragment = "baz" + }, + ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) + end}, + {"variation3", fun() -> + URI = "https://www.google.com/search", + Expect = #uri{ + scheme = "https", + authority = {undefined, "www.google.com", 443}, + path = "/search", + query = "", + fragment = undefined + }, + ?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI)) + end}, + {"variation4", fun() -> + URI = "https://www.google.com/search?foo=true", + 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} + ]. diff --git a/deps/rabbitmq_aws/test/rabbitmq_aws_xml_tests.erl b/deps/rabbitmq_aws/test/rabbitmq_aws_xml_tests.erl index 02c044be900d..9b64fea293b2 100644 --- a/deps/rabbitmq_aws/test/rabbitmq_aws_xml_tests.erl +++ b/deps/rabbitmq_aws/test/rabbitmq_aws_xml_tests.erl @@ -3,36 +3,48 @@ -include_lib("eunit/include/eunit.hrl"). parse_test_() -> - [ - {"s3 error response", fun() -> - Response = "\nSignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your key and signing method.AKIAIPPU25E5RA4MIYKQAWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e41 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 37GET\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\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85547 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 358EB36F450B78C45DIYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=", - Expectation = [{"Error", [ - {"Code", "SignatureDoesNotMatch"}, - {"Message", "The request signature we calculated does not match the signature you provided. Check your key and signing method."}, - {"AWSAccessKeyId", "AKIAIPPU25E5RA4MIYKQ"}, - {"StringToSign", "AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7"}, - {"SignatureProvided", "841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e"}, - {"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"}, - {"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"}, - {"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"}, - {"RequestId","8EB36F450B78C45D"}, - {"HostId", "IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw="} - ]}], - ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) - end}, - {"whitespace", fun() -> - Response = "\n value\n \n", - Expectation = [{"test", [{"example", "value"}]}], - ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) - end}, - {"multiple items", fun() -> - Response = "\nvaluevalue2\n \n", - Expectation = [{"test", [{"values", [{"example", "value"}, {"example", "value2"}]}]}], - ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) - end}, - {"small snippert", fun() -> - Response = "\nvalue", - Expectation = [{"test", "value"}], - ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) - end} - ]. + [ + {"s3 error response", fun() -> + Response = + "\nSignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your key and signing method.AKIAIPPU25E5RA4MIYKQAWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e41 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 37GET\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\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85547 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 358EB36F450B78C45DIYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=", + Expectation = [ + {"Error", [ + {"Code", "SignatureDoesNotMatch"}, + {"Message", + "The request signature we calculated does not match the signature you provided. Check your key and signing method."}, + {"AWSAccessKeyId", "AKIAIPPU25E5RA4MIYKQ"}, + {"StringToSign", + "AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7"}, + {"SignatureProvided", + "841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e"}, + {"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"}, + {"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"}, + {"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"}, + {"RequestId", "8EB36F450B78C45D"}, + {"HostId", + "IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw="} + ]} + ], + ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) + end}, + {"whitespace", fun() -> + Response = + "\n value\n \n", + Expectation = [{"test", [{"example", "value"}]}], + ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) + end}, + {"multiple items", fun() -> + Response = + "\nvaluevalue2\n \n", + Expectation = [{"test", [{"values", [{"example", "value"}, {"example", "value2"}]}]}], + ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) + end}, + {"small snippert", fun() -> + Response = "\nvalue", + Expectation = [{"test", "value"}], + ?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response)) + end} + ]. From 289acdbf4dc10ee7ec6591cf164f35079f326ed7 Mon Sep 17 00:00:00 2001 From: Simon Unge Date: Mon, 4 Aug 2025 14:16:23 +0000 Subject: [PATCH 2/2] Ignore format commit --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 8bee5ce4d1c1..2b6500310aad 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,5 @@ +# Format rabbitmq_aws with erlfmt +a4fffbd7e0a312fef2e514ade54fc4310a681542 # Revert "Format MQTT code with erlfmt" 209f23fa2f58e0240116b3e8e5be9cd54d34b569 # Format MQTT code with erlfmt