erlfmt entire plugin
This commit is contained in:
parent
94b4a6aafd
commit
a4fffbd7e0
|
@ -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,
|
||||
-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}).
|
||||
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(),
|
||||
-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()}).
|
||||
fragment :: undefined | fragment()
|
||||
}).
|
||||
|
||||
-type method() :: head | get | put | post | trace | options | delete | patch.
|
||||
-type http_version() :: string().
|
||||
|
@ -104,19 +107,20 @@
|
|||
|
||||
-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(),
|
||||
-record(request, {
|
||||
access_key :: access_key(),
|
||||
secret_access_key :: secret_access_key(),
|
||||
security_token :: security_token(),
|
||||
service :: string(),
|
||||
|
@ -124,15 +128,19 @@
|
|||
method = get :: method(),
|
||||
headers = [] :: headers(),
|
||||
uri :: string(),
|
||||
body = "" :: body()}).
|
||||
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().
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
-behavior(gen_server).
|
||||
|
||||
%% API exports
|
||||
-export([get/2, get/3,
|
||||
-export([
|
||||
get/2, get/3,
|
||||
post/4,
|
||||
refresh_credentials/0,
|
||||
request/5, request/6, request/7,
|
||||
|
@ -17,16 +18,19 @@
|
|||
has_credentials/0,
|
||||
set_region/1,
|
||||
ensure_imdsv2_token_valid/0,
|
||||
api_get_request/2]).
|
||||
api_get_request/2
|
||||
]).
|
||||
|
||||
%% gen-server exports
|
||||
-export([start_link/0,
|
||||
-export([
|
||||
start_link/0,
|
||||
init/1,
|
||||
terminate/2,
|
||||
code_change/3,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2]).
|
||||
handle_info/2
|
||||
]).
|
||||
|
||||
%% Export all for unit tests
|
||||
-ifdef(TEST).
|
||||
|
@ -40,8 +44,10 @@
|
|||
%% 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.
|
||||
|
@ -49,10 +55,11 @@
|
|||
get(Service, Path) ->
|
||||
get(Service, Path, []).
|
||||
|
||||
|
||||
-spec get(Service :: string(),
|
||||
-spec get(
|
||||
Service :: string(),
|
||||
Path :: path(),
|
||||
Headers :: headers()) -> result().
|
||||
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.
|
||||
|
@ -60,11 +67,12 @@ get(Service, Path) ->
|
|||
get(Service, Path, Headers) ->
|
||||
request(Service, get, Path, "", Headers).
|
||||
|
||||
|
||||
-spec post(Service :: string(),
|
||||
-spec post(
|
||||
Service :: string(),
|
||||
Path :: path(),
|
||||
Body :: body(),
|
||||
Headers :: headers()) -> result().
|
||||
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.
|
||||
|
@ -72,14 +80,12 @@ get(Service, Path, Headers) ->
|
|||
post(Service, 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).
|
||||
|
||||
|
||||
-spec refresh_credentials(state()) -> ok | error.
|
||||
%% @doc Manually refresh the credentials from the environment, filesystem or EC2 Instance Metadata Service.
|
||||
%% @end
|
||||
|
@ -89,12 +95,13 @@ refresh_credentials(State) ->
|
|||
?LOG_DEBUG("AWS credentials have been refreshed"),
|
||||
set_credentials(NewState).
|
||||
|
||||
|
||||
-spec request(Service :: string(),
|
||||
-spec request(
|
||||
Service :: string(),
|
||||
Method :: method(),
|
||||
Path :: path(),
|
||||
Body :: body(),
|
||||
Headers :: headers()) -> result().
|
||||
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.
|
||||
|
@ -102,35 +109,41 @@ refresh_credentials(State) ->
|
|||
request(Service, Method, Path, Body, Headers) ->
|
||||
gen_server:call(rabbitmq_aws, {request, Service, Method, Headers, Path, Body, [], undefined}).
|
||||
|
||||
|
||||
-spec request(Service :: string(),
|
||||
-spec request(
|
||||
Service :: string(),
|
||||
Method :: method(),
|
||||
Path :: path(),
|
||||
Body :: body(),
|
||||
Headers :: headers(),
|
||||
HTTPOptions :: http_options()) -> result().
|
||||
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}).
|
||||
gen_server:call(
|
||||
rabbitmq_aws, {request, Service, Method, Headers, Path, Body, HTTPOptions, undefined}
|
||||
).
|
||||
|
||||
|
||||
-spec request(Service :: string(),
|
||||
-spec request(
|
||||
Service :: string(),
|
||||
Method :: method(),
|
||||
Path :: path(),
|
||||
Body :: body(),
|
||||
Headers :: headers(),
|
||||
HTTPOptions :: http_options(),
|
||||
Endpoint :: host()) -> result().
|
||||
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) ->
|
||||
|
@ -145,7 +158,6 @@ set_credentials(NewState) ->
|
|||
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
|
||||
|
@ -158,7 +170,6 @@ set_region(Region) ->
|
|||
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
|
||||
|
@ -166,7 +177,6 @@ get_imdsv2_token() ->
|
|||
{ok, Imdsv2Token} = gen_server:call(rabbitmq_aws, get_imdsv2_token),
|
||||
Imdsv2Token.
|
||||
|
||||
|
||||
%%====================================================================
|
||||
%% gen_server functions
|
||||
%%====================================================================
|
||||
|
@ -174,16 +184,13 @@ get_imdsv2_token() ->
|
|||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
|
||||
-spec init(list()) -> {ok, state()}.
|
||||
init([]) ->
|
||||
{ok, #state{}}.
|
||||
|
||||
|
||||
terminate(_, _) ->
|
||||
ok.
|
||||
|
||||
|
||||
code_change(_, _, State) ->
|
||||
{ok, State}.
|
||||
|
||||
|
@ -193,7 +200,6 @@ handle_call(Msg, _From, State) ->
|
|||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
@ -201,48 +207,48 @@ handle_info(_Info, 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,
|
||||
{reply, ok, State#state{
|
||||
access_key = AccessKey,
|
||||
secret_access_key = SecretAccessKey,
|
||||
security_token = undefined,
|
||||
expiration = undefined,
|
||||
error = undefined}};
|
||||
|
||||
error = undefined
|
||||
}};
|
||||
handle_msg({set_credentials, NewState}, State) ->
|
||||
{reply, ok, State#state{access_key = NewState#state.access_key,
|
||||
{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}};
|
||||
|
||||
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
|
||||
|
@ -251,7 +257,6 @@ endpoint(#state{region = Region}, undefined, Service, Path) ->
|
|||
endpoint(_, 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.
|
||||
|
@ -259,7 +264,6 @@ endpoint(_, Host, _, Path) ->
|
|||
endpoint_host(Region, Service) ->
|
||||
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.
|
||||
%% See https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region for details.
|
||||
|
@ -288,10 +292,12 @@ format_response({error, Reason}) ->
|
|||
%% {Type, Subtype}.
|
||||
%% @end
|
||||
get_content_type(Headers) ->
|
||||
Value = case proplists:get_value("content-type", Headers, undefined) of
|
||||
Value =
|
||||
case proplists:get_value("content-type", Headers, undefined) of
|
||||
undefined ->
|
||||
proplists:get_value("Content-Type", Headers, "text/xml");
|
||||
Other -> Other
|
||||
Other ->
|
||||
Other
|
||||
end,
|
||||
parse_content_type(Value).
|
||||
|
||||
|
@ -307,17 +313,16 @@ 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.
|
||||
|
||||
|
||||
-spec load_credentials(State :: state()) -> {ok, state()} | {error, state()}.
|
||||
%% @doc Load the credentials using the following order of configuration precedence:
|
||||
%% - Environment variables
|
||||
|
@ -327,25 +332,31 @@ expired_credentials(Expiration) ->
|
|||
load_credentials(#state{region = Region}) ->
|
||||
case rabbitmq_aws_config:credentials() of
|
||||
{ok, AccessKey, SecretAccessKey, Expiration, SecurityToken} ->
|
||||
{ok, #state{region = Region,
|
||||
{ok, #state{
|
||||
region = Region,
|
||||
error = undefined,
|
||||
access_key = AccessKey,
|
||||
secret_access_key = SecretAccessKey,
|
||||
expiration = Expiration,
|
||||
security_token = SecurityToken,
|
||||
imdsv2_token = undefined}};
|
||||
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,
|
||||
?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}}
|
||||
imdsv2_token = undefined
|
||||
}}
|
||||
end.
|
||||
|
||||
|
||||
-spec local_time() -> calendar:datetime().
|
||||
%% @doc Return the current local time.
|
||||
%% @end
|
||||
|
@ -353,8 +364,8 @@ local_time() ->
|
|||
[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) ->
|
||||
|
@ -366,7 +377,6 @@ maybe_decode_body({_, "xml"}, Body) ->
|
|||
maybe_decode_body(_ContentType, 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
|
||||
|
@ -375,39 +385,75 @@ parse_content_type(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()}.
|
||||
-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).
|
||||
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()}.
|
||||
-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()}.
|
||||
-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.
|
||||
|
@ -417,11 +463,17 @@ perform_request_creds_expired(false, State, Service, Method, Headers, Path, Body
|
|||
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()}.
|
||||
-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
|
||||
|
@ -431,11 +483,16 @@ perform_request_with_creds(State, Service, Method, Headers, Path, Body, Options,
|
|||
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()}.
|
||||
-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
|
||||
|
@ -448,7 +505,6 @@ perform_request_with_creds(State, Method, URI, Headers, ContentType, Body, Optio
|
|||
Response = httpc:request(Method, {URI, Headers, ContentType, Body}, Options1, []),
|
||||
{format_response(Response), State}.
|
||||
|
||||
|
||||
-spec perform_request_creds_error(State :: state()) ->
|
||||
{result_error(), NewState :: state()}.
|
||||
%% @doc Return the error response when there are not any credentials to use with
|
||||
|
@ -457,7 +513,6 @@ perform_request_with_creds(State, Method, URI, Headers, ContentType, Body, Optio
|
|||
perform_request_creds_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
|
||||
%% enough time for a long connect and request phase to succeed.
|
||||
|
@ -474,16 +529,31 @@ 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,
|
||||
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,
|
||||
region = Region
|
||||
},
|
||||
Service,
|
||||
Method,
|
||||
URI,
|
||||
Headers,
|
||||
Body
|
||||
) ->
|
||||
rabbitmq_aws_sign:headers(#request{
|
||||
access_key = AccessKey,
|
||||
secret_access_key = SecretKey,
|
||||
security_token = SecurityToken,
|
||||
region = Region,
|
||||
|
@ -491,7 +561,8 @@ sign_headers(#state{access_key = AccessKey,
|
|||
method = Method,
|
||||
uri = URI,
|
||||
headers = Headers,
|
||||
body = Body}).
|
||||
body = Body
|
||||
}).
|
||||
|
||||
-spec expired_imdsv2_token('undefined' | imdsv2token()) -> boolean().
|
||||
%% @doc Determine whether or not an Imdsv2Token has expired.
|
||||
|
@ -508,17 +579,21 @@ expired_imdsv2_token({_, _, 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}),
|
||||
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
|
||||
_ ->
|
||||
Imdsv2Token#imdsv2token.token
|
||||
end.
|
||||
|
||||
-spec ensure_credentials_valid() -> ok.
|
||||
|
@ -530,14 +605,15 @@ 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 ->
|
||||
case expired_credentials(State#state.expiration) of
|
||||
true -> refresh_credentials(State);
|
||||
_ -> ok
|
||||
end;
|
||||
_ -> refresh_credentials(State)
|
||||
_ ->
|
||||
refresh_credentials(State)
|
||||
end.
|
||||
|
||||
|
||||
-spec api_get_request(string(), path()) -> {'ok', list()} | {'error', term()}.
|
||||
%% @doc Invoke an API call to an AWS service.
|
||||
%% @end
|
||||
|
@ -545,8 +621,8 @@ 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).
|
||||
|
||||
|
||||
-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, _) ->
|
||||
|
@ -555,13 +631,18 @@ api_get_request_with_retries(_, _, 0, _) ->
|
|||
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, {_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]),
|
||||
{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
|
||||
{_, 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),
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
-module(rabbitmq_aws_config).
|
||||
|
||||
%% API
|
||||
-export([credentials/0,
|
||||
-export([
|
||||
credentials/0,
|
||||
credentials/1,
|
||||
value/2,
|
||||
values/1,
|
||||
|
@ -22,7 +23,8 @@
|
|||
load_imdsv2_token/0,
|
||||
instance_metadata_request_headers/0,
|
||||
region/0,
|
||||
region/1]).
|
||||
region/1
|
||||
]).
|
||||
|
||||
%% Export all for unit tests
|
||||
-ifdef(TEST).
|
||||
|
@ -129,10 +131,11 @@ credentials() ->
|
|||
%% will be returned.
|
||||
%% @end
|
||||
credentials(Profile) ->
|
||||
lookup_credentials(Profile,
|
||||
lookup_credentials(
|
||||
Profile,
|
||||
os:getenv("AWS_ACCESS_KEY_ID"),
|
||||
os:getenv("AWS_SECRET_ACCESS_KEY")).
|
||||
|
||||
os:getenv("AWS_SECRET_ACCESS_KEY")
|
||||
).
|
||||
|
||||
-spec region() -> {ok, string()}.
|
||||
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
|
||||
|
@ -146,7 +149,6 @@ credentials(Profile) ->
|
|||
region() ->
|
||||
region(profile()).
|
||||
|
||||
|
||||
-spec region(Region :: string()) -> {ok, region()}.
|
||||
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
|
||||
%% variable or as configured in the configuration file using the specified
|
||||
|
@ -162,7 +164,6 @@ region(Profile) ->
|
|||
_ -> {ok, ?DEFAULT_REGION}
|
||||
end.
|
||||
|
||||
|
||||
-spec instance_id() -> {'ok', string()} | {'error', 'undefined'}.
|
||||
%% @doc Return the instance ID from the EC2 metadata service.
|
||||
%% @end
|
||||
|
@ -170,18 +171,17 @@ instance_id() ->
|
|||
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)).
|
||||
|
||||
|
||||
-spec values(Profile :: string())
|
||||
-> Settings :: list()
|
||||
-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.
|
||||
|
@ -192,17 +192,21 @@ values(Profile) ->
|
|||
{error, Reason};
|
||||
Settings ->
|
||||
Prefixed = lists:flatten(["profile ", Profile]),
|
||||
proplists:get_value(Profile, Settings,
|
||||
proplists:get_value(Prefixed,
|
||||
Settings, {error, undefined}))
|
||||
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
|
||||
|
@ -211,7 +215,6 @@ values(Profile) ->
|
|||
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
|
||||
%% AWS_CONFIG_FILE or the default location where the file is expected to
|
||||
|
@ -222,14 +225,12 @@ config_file(false) ->
|
|||
config_file(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()).
|
||||
|
||||
|
||||
-spec credentials_file() -> string().
|
||||
%% @doc Return the shared credentials file to test using either the value of the
|
||||
%% AWS_SHARED_CREDENTIALS_FILE or the default location where the file
|
||||
|
@ -238,7 +239,6 @@ config_file_data() ->
|
|||
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
|
||||
%% AWS_SHARED_CREDENTIALS_FILE or the default location where the file
|
||||
|
@ -255,15 +255,15 @@ credentials_file(EnvVar) ->
|
|||
credentials_file_data() ->
|
||||
ini_file_data(credentials_file()).
|
||||
|
||||
|
||||
-spec get_value(Key :: atom(), Settings :: list()) -> any();
|
||||
-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}.
|
||||
|
||||
get_value(_, {error, Reason}) ->
|
||||
{error, Reason}.
|
||||
|
||||
-spec home_path() -> string().
|
||||
%% @doc Return the path to the current user's home directory, checking for the
|
||||
|
@ -273,7 +273,6 @@ get_value(_, {error, Reason}) -> {error, Reason}.
|
|||
home_path() ->
|
||||
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
|
||||
%% HOME environment variable before returning the current working
|
||||
|
@ -282,17 +281,15 @@ 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)).
|
||||
|
||||
|
||||
-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) ->
|
||||
|
@ -300,8 +297,8 @@ ini_file_data(Path, true) ->
|
|||
{ok, Lines} -> ini_parse_lines(Lines, none, none, []);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end;
|
||||
ini_file_data(_, false) -> {error, enoent}.
|
||||
|
||||
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
|
||||
|
@ -312,11 +309,12 @@ ini_format_key(Key) ->
|
|||
false -> {error, type}
|
||||
end.
|
||||
|
||||
|
||||
-spec ini_parse_line(Section :: list(),
|
||||
-spec ini_parse_line(
|
||||
Section :: list(),
|
||||
Key :: atom(),
|
||||
Line :: binary())
|
||||
-> {Section :: list(), Key :: string() | none}.
|
||||
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>>) ->
|
||||
|
@ -329,53 +327,68 @@ ini_parse_line(Section, _, Line) ->
|
|||
{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)})};
|
||||
ini_parse_line_parts(_, [RawKey]) ->
|
||||
{new_parent, ini_format_key(RawKey)}.
|
||||
|
||||
|
||||
-spec ini_parse_lines(Lines::[binary()],
|
||||
-spec ini_parse_lines(
|
||||
Lines :: [binary()],
|
||||
SectionName :: string() | atom(),
|
||||
Parent :: atom(),
|
||||
Accumulator :: list())
|
||||
-> list().
|
||||
Accumulator :: list()
|
||||
) ->
|
||||
list().
|
||||
%% @doc Parse the AWS configuration INI file
|
||||
%% @end
|
||||
ini_parse_lines([], _, _, Settings) -> Settings;
|
||||
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),
|
||||
{ok, NewParent, NewSettings} = ini_parse_section(
|
||||
H,
|
||||
NewSectionName,
|
||||
Parent,
|
||||
Settings
|
||||
),
|
||||
ini_parse_lines(T, NewSectionName, NewParent, NewSettings).
|
||||
|
||||
|
||||
-spec ini_parse_section(Line :: binary(),
|
||||
-spec ini_parse_section(
|
||||
Line :: binary(),
|
||||
SectionName :: string(),
|
||||
Parent :: atom(),
|
||||
Section :: list())
|
||||
-> {ok, NewParent :: atom(), Section :: list()}.
|
||||
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})}.
|
||||
{ok, NewParent,
|
||||
lists:keystore(
|
||||
SectionName,
|
||||
1,
|
||||
Settings,
|
||||
{SectionName, NewSection}
|
||||
)}.
|
||||
|
||||
|
||||
-spec ini_parse_section_name(CurrentSection :: string() | atom(),
|
||||
Line :: binary())
|
||||
-> {ok, SectionName :: string()}.
|
||||
-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
|
||||
|
@ -386,14 +399,12 @@ ini_parse_section_name(CurrentSection, Line) ->
|
|||
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)), "=").
|
||||
|
||||
|
||||
-spec instance_availability_zone_url() -> string().
|
||||
%% @doc Return the URL for querying the availability zone from the Instance
|
||||
%% Metadata service
|
||||
|
@ -401,7 +412,6 @@ ini_split_line(Line) ->
|
|||
instance_availability_zone_url() ->
|
||||
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
|
||||
|
@ -409,15 +419,16 @@ instance_availability_zone_url() ->
|
|||
instance_credentials_url(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,
|
||||
rabbitmq_aws_urilib:build(#uri{
|
||||
scheme = http,
|
||||
authority = {undefined, ?INSTANCE_HOST, undefined},
|
||||
path = Path, query = []}).
|
||||
|
||||
path = Path,
|
||||
query = []
|
||||
}).
|
||||
|
||||
-spec instance_role_url() -> string().
|
||||
%% @doc Return the URL for querying the role associated with the current
|
||||
|
@ -438,31 +449,37 @@ imdsv2_token_url() ->
|
|||
instance_id_url() ->
|
||||
instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_ID], "/")).
|
||||
|
||||
|
||||
-spec lookup_credentials(Profile :: string(),
|
||||
-spec lookup_credentials(
|
||||
Profile :: string(),
|
||||
AccessKey :: string() | false,
|
||||
SecretKey :: string() | false)
|
||||
-> security_credentials().
|
||||
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,
|
||||
lookup_credentials_from_config(
|
||||
Profile,
|
||||
value(Profile, aws_access_key_id),
|
||||
value(Profile, aws_secret_access_key));
|
||||
value(Profile, aws_secret_access_key)
|
||||
);
|
||||
lookup_credentials(Profile, _, false) ->
|
||||
lookup_credentials_from_config(Profile,
|
||||
lookup_credentials_from_config(
|
||||
Profile,
|
||||
value(Profile, aws_access_key_id),
|
||||
value(Profile, aws_secret_access_key));
|
||||
value(Profile, aws_secret_access_key)
|
||||
);
|
||||
lookup_credentials(_, AccessKey, SecretKey) ->
|
||||
{ok, AccessKey, SecretKey, undefined, undefined}.
|
||||
|
||||
|
||||
-spec lookup_credentials_from_config(Profile :: string(),
|
||||
-spec lookup_credentials_from_config(
|
||||
Profile :: string(),
|
||||
access_key() | {error, Reason :: atom()},
|
||||
secret_access_key()| {error, Reason :: atom()})
|
||||
-> security_credentials().
|
||||
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
|
||||
|
@ -473,10 +490,11 @@ lookup_credentials_from_config(Profile, {error,_}, _) ->
|
|||
lookup_credentials_from_config(_, AccessKey, SecretKey) ->
|
||||
{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,
|
||||
|
@ -488,9 +506,8 @@ lookup_credentials_from_file(Profile, Credentials) ->
|
|||
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
|
||||
|
@ -503,10 +520,11 @@ lookup_credentials_from_section(Credentials) ->
|
|||
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().
|
||||
-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
|
||||
|
@ -517,9 +535,8 @@ lookup_credentials_from_proplist(_, undefined) ->
|
|||
lookup_credentials_from_proplist(AccessKey, SecretKey) ->
|
||||
{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
|
||||
|
@ -527,20 +544,21 @@ 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}.
|
||||
-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(_, 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.
|
||||
|
@ -550,9 +568,8 @@ lookup_region_from_config({error, _}) ->
|
|||
lookup_region_from_config(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
|
||||
|
@ -561,7 +578,6 @@ lookup_region_from_settings(undefined) ->
|
|||
lookup_region_from_settings(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().
|
||||
|
@ -575,13 +591,15 @@ maybe_convert_number(Value) ->
|
|||
catch
|
||||
error:badarg -> Stripped
|
||||
end;
|
||||
{F,_Rest} -> F
|
||||
{F, _Rest} ->
|
||||
F
|
||||
end.
|
||||
|
||||
|
||||
-spec maybe_get_credentials_from_instance_metadata({ok, Role :: string()} |
|
||||
{error, undefined})
|
||||
-> {'ok', security_credentials()} | {'error', term()}.
|
||||
-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
|
||||
|
@ -591,16 +609,14 @@ maybe_get_credentials_from_instance_metadata({ok, Role}) ->
|
|||
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)).
|
||||
|
||||
|
||||
%% @doc Try to query the EC2 local instance metadata service to get the role
|
||||
%% assigned to the instance.
|
||||
%% @end
|
||||
|
@ -608,56 +624,66 @@ maybe_get_role_from_instance_metadata() ->
|
|||
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.")),
|
||||
?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.")),
|
||||
?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}.
|
||||
|
||||
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),
|
||||
{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}], []).
|
||||
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.
|
||||
|
@ -667,7 +693,6 @@ get_instruction_on_instance_metadata_error(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
|
||||
|
@ -677,9 +702,9 @@ 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))}}.
|
||||
|
||||
{{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,7 +726,8 @@ 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().
|
||||
|
@ -712,36 +736,48 @@ read_file(Path) ->
|
|||
region_from_availability_zone(Value) ->
|
||||
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
|
||||
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."),
|
||||
?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]),
|
||||
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}
|
||||
{ok, false} ->
|
||||
[];
|
||||
%% undefined or {ok, true}
|
||||
_ ->
|
||||
?LOG_DEBUG("EC2 Instance Metadata Service v2 (IMDSv2) is preferred."),
|
||||
maybe_imdsv2_token_headers()
|
||||
end.
|
||||
|
|
|
@ -21,23 +21,27 @@ decode(Value) when is_binary(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([], 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), [])}]));
|
||||
[{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, [])}]));
|
||||
[{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) ->
|
||||
|
|
|
@ -28,27 +28,32 @@ headers(Request) ->
|
|||
PayloadHash = sha256(Request#request.body),
|
||||
URI = rabbitmq_aws_urilib:parse(Request#request.uri),
|
||||
{_, Host, _} = URI#uri.authority,
|
||||
Headers = append_headers(RequestTimestamp,
|
||||
Headers = append_headers(
|
||||
RequestTimestamp,
|
||||
length(Request#request.body),
|
||||
PayloadHash,
|
||||
Host,
|
||||
Request#request.security_token,
|
||||
Request#request.headers),
|
||||
RequestHash = request_hash(Request#request.method,
|
||||
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.body
|
||||
),
|
||||
AuthValue = authorization(
|
||||
Request#request.access_key,
|
||||
Request#request.secret_access_key,
|
||||
RequestTimestamp,
|
||||
Request#request.region,
|
||||
Request#request.service,
|
||||
Headers,
|
||||
RequestHash),
|
||||
RequestHash
|
||||
),
|
||||
sort_headers(lists:merge([{"authorization", AuthValue}], Headers)).
|
||||
|
||||
|
||||
-spec amz_date(AMZTimestamp :: string()) -> string().
|
||||
%% @doc Extract the date from the AMZ timestamp format.
|
||||
%% @end
|
||||
|
@ -56,31 +61,40 @@ amz_date(AMZTimestamp) ->
|
|||
[RequestDate, _] = string:tokens(AMZTimestamp, "T"),
|
||||
RequestDate.
|
||||
|
||||
|
||||
-spec append_headers(AMZDate :: string(),
|
||||
-spec append_headers(
|
||||
AMZDate :: string(),
|
||||
ContentLength :: integer(),
|
||||
PayloadHash :: string(),
|
||||
Hostname :: host(),
|
||||
SecurityToken :: security_token(),
|
||||
Headers :: headers()) -> list().
|
||||
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]).
|
||||
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(),
|
||||
-spec authorization(
|
||||
AccessKey :: access_key(),
|
||||
SecretAccessKey :: secret_access_key(),
|
||||
RequestTimestamp :: string(),
|
||||
Region :: region(),
|
||||
Service :: string(),
|
||||
Headers :: headers(),
|
||||
RequestHash :: string()) -> string().
|
||||
RequestHash :: string()
|
||||
) -> string().
|
||||
%% @doc Return the authorization header value
|
||||
%% @end
|
||||
authorization(AccessKey, SecretAccessKey, RequestTimestamp, Region, Service, Headers, RequestHash) ->
|
||||
|
@ -93,27 +107,31 @@ authorization(AccessKey, SecretAccessKey, RequestTimestamp, Region, Service, Hea
|
|||
Signature = string:join(["Signature", signature(StringToSign, SigningKey)], "="),
|
||||
string:join([Credentials, SignedHeaders, Signature], ", ").
|
||||
|
||||
|
||||
-spec default_headers(RequestTimestamp :: string(),
|
||||
-spec default_headers(
|
||||
RequestTimestamp :: string(),
|
||||
ContentLength :: integer(),
|
||||
PayloadHash :: string(),
|
||||
Hostname :: host(),
|
||||
SecurityToken :: security_token()) -> headers().
|
||||
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)},
|
||||
[
|
||||
{"content-length", integer_to_list(ContentLength)},
|
||||
{"date", RequestTimestamp},
|
||||
{"host", Hostname},
|
||||
{"x-amz-content-sha256", PayloadHash}];
|
||||
{"x-amz-content-sha256", PayloadHash}
|
||||
];
|
||||
default_headers(RequestTimestamp, ContentLength, PayloadHash, Hostname, SecurityToken) ->
|
||||
[{"content-length", integer_to_list(ContentLength)},
|
||||
[
|
||||
{"content-length", integer_to_list(ContentLength)},
|
||||
{"date", RequestTimestamp},
|
||||
{"host", Hostname},
|
||||
{"x-amz-content-sha256", PayloadHash},
|
||||
{"x-amz-security-token", SecurityToken}].
|
||||
|
||||
{"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
|
||||
|
@ -132,26 +150,27 @@ 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(),
|
||||
-spec credential_scope(
|
||||
RequestDate :: string(),
|
||||
Region :: region(),
|
||||
Service :: string()) -> string().
|
||||
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"], "/")).
|
||||
|
||||
|
||||
-spec header_value(Key :: string(),
|
||||
-spec header_value(
|
||||
Key :: string(),
|
||||
Headers :: headers(),
|
||||
Default :: string()) -> string().
|
||||
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)).
|
||||
|
||||
|
||||
-spec hmac_sign(Key :: string(), Message :: string()) -> string().
|
||||
%% @doc Return the SHA-256 hash for the specified value.
|
||||
%% @end
|
||||
|
@ -159,7 +178,6 @@ hmac_sign(Key, Message) ->
|
|||
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
|
||||
|
@ -167,60 +185,67 @@ local_time() ->
|
|||
[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])).
|
||||
|
||||
|
||||
-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)).
|
||||
query_string(QueryArgs) -> rabbitmq_aws_urilib:build_query_string(lists:keysort(1, QueryArgs)).
|
||||
|
||||
|
||||
-spec request_hash(Method :: method(),
|
||||
-spec request_hash(
|
||||
Method :: method(),
|
||||
Path :: path(),
|
||||
QArgs :: query_args(),
|
||||
Headers :: headers(),
|
||||
Payload :: string()) -> string().
|
||||
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
|
||||
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)),
|
||||
CanonicalRequest = string:join(
|
||||
[
|
||||
string:to_upper(atom_to_list(Method)),
|
||||
EncodedPath,
|
||||
query_string(QArgs),
|
||||
canonical_headers(Headers),
|
||||
signed_headers(Headers),
|
||||
sha256(Payload)], "\n"),
|
||||
sha256(Payload)
|
||||
],
|
||||
"\n"
|
||||
),
|
||||
sha256(CanonicalRequest).
|
||||
|
||||
|
||||
-spec scope(AMZDate :: string(),
|
||||
-spec scope(
|
||||
AMZDate :: string(),
|
||||
Region :: region(),
|
||||
Service :: string()) -> string().
|
||||
Service :: string()
|
||||
) -> string().
|
||||
%% @doc Create the Scope string
|
||||
%% @end
|
||||
scope(AMZDate, Region, Service) ->
|
||||
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
|
||||
|
@ -228,28 +253,30 @@ sha256(Value) ->
|
|||
signed_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([], SignedHeaders) ->
|
||||
string:join(SignedHeaders, ";");
|
||||
signed_headers([{Key, _} | T], SignedHeaders) ->
|
||||
signed_headers(T, SignedHeaders ++ [string:to_lower(Key)]).
|
||||
|
||||
|
||||
-spec signature(StringToSign :: string(),
|
||||
SigningKey :: string()) -> string().
|
||||
-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(),
|
||||
-spec signing_key(
|
||||
SecretKey :: secret_access_key(),
|
||||
AMZDate :: string(),
|
||||
Region :: region(),
|
||||
Service :: string()) -> string().
|
||||
Service :: string()
|
||||
) -> string().
|
||||
%% @doc Create the signing key
|
||||
%% @end
|
||||
signing_key(SecretKey, AMZDate, Region, Service) ->
|
||||
|
@ -258,23 +285,28 @@ signing_key(SecretKey, AMZDate, Region, Service) ->
|
|||
ServiceKey = hmac_sign(RegionKey, Service),
|
||||
hmac_sign(ServiceKey, "aws4_request").
|
||||
|
||||
|
||||
-spec string_to_sign(RequestTimestamp :: string(),
|
||||
-spec string_to_sign(
|
||||
RequestTimestamp :: string(),
|
||||
RequestDate :: string(),
|
||||
Region :: region(),
|
||||
Service :: string(),
|
||||
RequestHash :: string()) -> 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([
|
||||
lists:flatten(
|
||||
string:join(
|
||||
[
|
||||
?ALGORITHM,
|
||||
RequestTimestamp,
|
||||
CredentialScope,
|
||||
RequestHash
|
||||
], "\n")).
|
||||
|
||||
],
|
||||
"\n"
|
||||
)
|
||||
).
|
||||
|
||||
-spec sort_headers(Headers :: headers()) -> headers().
|
||||
%% @doc Case-insensitive sorting of the request headers
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
-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]}).
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
%% ====================================================================
|
||||
-module(rabbitmq_aws_urilib).
|
||||
|
||||
-export([build/1,
|
||||
-export([
|
||||
build/1,
|
||||
build_query_string/1,
|
||||
parse/1,
|
||||
parse_userinfo/1,
|
||||
|
@ -30,30 +31,37 @@ build(URI) ->
|
|||
scheme => to_list(URI#uri.scheme),
|
||||
host => Host
|
||||
},
|
||||
UriMap1 = case UserInfo of
|
||||
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
|
||||
UriMap2 =
|
||||
case Port of
|
||||
undefined -> UriMap1;
|
||||
Value1 -> maps:put(port, Value1, UriMap1)
|
||||
end,
|
||||
UriMap3 = case URI#uri.path of
|
||||
undefined -> maps:put(path, "", UriMap2);
|
||||
UriMap3 =
|
||||
case URI#uri.path of
|
||||
undefined ->
|
||||
maps:put(path, "", UriMap2);
|
||||
Value2 ->
|
||||
PrefixedPath = case string:slice(Value2, 0, 1) of
|
||||
PrefixedPath =
|
||||
case string:slice(Value2, 0, 1) of
|
||||
"/" -> Value2;
|
||||
_ -> "/" ++ Value2
|
||||
end,
|
||||
maps:put(path, PrefixedPath, UriMap2)
|
||||
end,
|
||||
UriMap4 = case URI#uri.query of
|
||||
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
|
||||
UriMap5 =
|
||||
case URI#uri.fragment of
|
||||
undefined -> UriMap4;
|
||||
Value4 -> maps:put(fragment, Value4, UriMap4)
|
||||
end,
|
||||
|
@ -67,7 +75,8 @@ parse(Value) ->
|
|||
Scheme = maps:get(scheme, UriMap, "https"),
|
||||
Host = maps:get(host, UriMap),
|
||||
|
||||
DefaultPort = case Scheme of
|
||||
DefaultPort =
|
||||
case Scheme of
|
||||
"http" -> 80;
|
||||
"https" -> 443;
|
||||
_ -> undefined
|
||||
|
@ -76,26 +85,24 @@ parse(Value) ->
|
|||
UserInfo = parse_userinfo(maps:get(userinfo, UriMap, undefined)),
|
||||
Path = maps:get(path, UriMap),
|
||||
Query = maps:get(query, UriMap, ""),
|
||||
#uri{scheme = Scheme,
|
||||
#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(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};
|
||||
|
|
|
@ -15,32 +15,29 @@ parse(Value) ->
|
|||
{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)}].
|
||||
|
||||
|
||||
flatten_text([], Value) -> Value;
|
||||
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)
|
||||
Stripped -> lists:append([Stripped], Accum)
|
||||
end;
|
||||
parse_content([H | T], Accum) ->
|
||||
parse_content(T, parse_content(H, Accum)).
|
||||
|
|
|
@ -13,12 +13,13 @@ start_test_() ->
|
|||
[
|
||||
{"supervisor initialized", fun() ->
|
||||
meck:expect(rabbitmq_aws_sup, start_link, fun() -> {ok, test_result} end),
|
||||
?assertEqual({ok, test_result},
|
||||
rabbitmq_aws_app:start(temporary, [])),
|
||||
?assertEqual(
|
||||
{ok, test_result},
|
||||
rabbitmq_aws_app:start(temporary, [])
|
||||
),
|
||||
meck:validate(rabbitmq_aws_sup)
|
||||
end}
|
||||
]
|
||||
}.
|
||||
]}.
|
||||
|
||||
stop_test() ->
|
||||
?assertEqual(ok, rabbitmq_aws_app:stop({})).
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
-include("rabbitmq_aws.hrl").
|
||||
|
||||
|
||||
config_file_test_() ->
|
||||
[
|
||||
{"from environment variable", fun() ->
|
||||
|
@ -14,8 +13,10 @@ config_file_test_() ->
|
|||
{"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())
|
||||
?assertEqual(
|
||||
"/home/rrabbit/.aws/config",
|
||||
rabbitmq_aws_config:config_file()
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
|
@ -24,59 +25,78 @@ config_file_data_test_() ->
|
|||
{"successfully parses ini", fun() ->
|
||||
setup_test_config_env_var(),
|
||||
Expectation = [
|
||||
{"default",
|
||||
[{aws_access_key_id, "default-key"},
|
||||
{"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"},
|
||||
{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"}]}
|
||||
{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())
|
||||
?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
|
||||
},
|
||||
?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
|
||||
}
|
||||
?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())
|
||||
?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())
|
||||
?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())
|
||||
?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"))
|
||||
?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}
|
||||
].
|
||||
|
||||
|
@ -89,12 +109,13 @@ credentials_file_test_() ->
|
|||
{"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())
|
||||
?assertEqual(
|
||||
"/home/rrabbit/.aws/credentials",
|
||||
rabbitmq_aws_config:credentials_file()
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
|
||||
credentials_test_() ->
|
||||
{
|
||||
foreach,
|
||||
|
@ -109,130 +130,175 @@ credentials_test_() ->
|
|||
{"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())
|
||||
?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())
|
||||
?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())
|
||||
?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())
|
||||
?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"))
|
||||
?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"))
|
||||
?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())
|
||||
?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"))
|
||||
?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"))
|
||||
?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"))
|
||||
?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"))
|
||||
?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"))
|
||||
?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}}]),
|
||||
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",
|
||||
Expectation =
|
||||
{ok, "ASIAIMAFAKEACCESSKEY", "2+t64tZZVaz0yp0x1G23ZRYn+FAKEyVALUEs/4qh",
|
||||
{{2016, 4, 1}, {4, 13, 28}}, "FAKE//////////wEAK/TOKEN/VALUE="},
|
||||
?assertEqual(Expectation, rabbitmq_aws_config:credentials())
|
||||
end
|
||||
},
|
||||
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
|
||||
},
|
||||
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"}}),
|
||||
meck:expect(
|
||||
httpc,
|
||||
request,
|
||||
4,
|
||||
{ok, {{protocol, 500, message}, headers, "Internal Server Error"}}
|
||||
),
|
||||
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
|
||||
end
|
||||
},
|
||||
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}]),
|
||||
meck:sequence(
|
||||
httpc,
|
||||
request,
|
||||
4,
|
||||
[
|
||||
{ok, {{protocol, 200, message}, headers, "Bob"}},
|
||||
{error, timeout}
|
||||
]
|
||||
),
|
||||
?assertEqual({error, undefined}, rabbitmq_aws_config:credentials())
|
||||
end
|
||||
},
|
||||
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"}}]),
|
||||
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
|
||||
}
|
||||
|
||||
]}.
|
||||
|
||||
end}
|
||||
]
|
||||
}.
|
||||
|
||||
home_path_test_() ->
|
||||
[
|
||||
{"with HOME", fun() ->
|
||||
os:putenv("HOME", "/home/rrabbit"),
|
||||
?assertEqual("/home/rrabbit",
|
||||
rabbitmq_aws_config:home_path())
|
||||
?assertEqual(
|
||||
"/home/rrabbit",
|
||||
rabbitmq_aws_config:home_path()
|
||||
)
|
||||
end},
|
||||
{"without HOME", fun() ->
|
||||
os:unsetenv("HOME"),
|
||||
?assertEqual(filename:absname("."),
|
||||
rabbitmq_aws_config:home_path())
|
||||
?assertEqual(
|
||||
filename:absname("."),
|
||||
rabbitmq_aws_config:home_path()
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
|
||||
ini_format_key_test_() ->
|
||||
[
|
||||
{"when value is list", fun() ->
|
||||
|
@ -243,7 +309,6 @@ ini_format_key_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
maybe_convert_number_test_() ->
|
||||
[
|
||||
{"when string contains an integer", fun() ->
|
||||
|
@ -257,7 +322,6 @@ maybe_convert_number_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
parse_iso8601_test_() ->
|
||||
[
|
||||
{"parse test", fun() ->
|
||||
|
@ -267,7 +331,6 @@ parse_iso8601_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
profile_test_() ->
|
||||
[
|
||||
{"from environment variable", fun() ->
|
||||
|
@ -280,15 +343,16 @@ profile_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
read_file_test_() ->
|
||||
[
|
||||
{"file does not exist", fun() ->
|
||||
?assertEqual({error, enoent}, rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"])))
|
||||
?assertEqual(
|
||||
{error, enoent},
|
||||
rabbitmq_aws_config:read_file(filename:join([filename:absname("."), "bad_path"]))
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
|
||||
region_test_() ->
|
||||
{
|
||||
foreach,
|
||||
|
@ -319,8 +383,12 @@ region_test_() ->
|
|||
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"}}),
|
||||
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() ->
|
||||
|
@ -329,12 +397,16 @@ region_test_() ->
|
|||
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"}}),
|
||||
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_() ->
|
||||
{
|
||||
|
@ -347,27 +419,27 @@ instance_id_test_() ->
|
|||
end,
|
||||
fun meck:unload/1,
|
||||
[
|
||||
{"get instance id successfully",
|
||||
fun() ->
|
||||
{"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"}}),
|
||||
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() ->
|
||||
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"}}),
|
||||
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() ->
|
||||
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"}}),
|
||||
meck:expect(
|
||||
httpc, request, 4, {error, {{protocol, 403, message}, headers, "access denied"}}
|
||||
),
|
||||
?assertEqual({error, undefined}, rabbitmq_aws_config:instance_id())
|
||||
end
|
||||
}
|
||||
end}
|
||||
]
|
||||
}.
|
||||
|
||||
|
@ -380,27 +452,36 @@ load_imdsv2_token_test_() ->
|
|||
end,
|
||||
fun meck:unload/1,
|
||||
[
|
||||
{"fail to get imdsv2 token - timeout",
|
||||
fun() ->
|
||||
{"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."}}),
|
||||
{"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() ->
|
||||
{"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}}]),
|
||||
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,
|
||||
|
@ -413,12 +494,15 @@ maybe_imdsv2_token_headers_test_() ->
|
|||
{"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}
|
||||
,
|
||||
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())
|
||||
?assertEqual(
|
||||
[{"X-aws-ec2-metadata-token", IMDSv2Token}],
|
||||
rabbitmq_aws_config:maybe_imdsv2_token_headers()
|
||||
)
|
||||
end}
|
||||
]
|
||||
}.
|
||||
|
@ -428,18 +512,27 @@ reset_environment() ->
|
|||
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"),
|
||||
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(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"
|
||||
).
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
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\"}]}",
|
||||
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"},
|
||||
{"tagSet", [
|
||||
{"resourceId", "i-13a4abea"},
|
||||
{"resourceType", "instance"},
|
||||
{"key", "Environment"},
|
||||
{"value", "prod-us-east-1"},
|
||||
|
@ -22,23 +23,35 @@ parse_test_() ->
|
|||
{"resourceId", "i-13a4abea"},
|
||||
{"resourceType", "instance"},
|
||||
{"key", "aws:cloudformation:stack-name"},
|
||||
{"value","prod-us-east-1-ecs-1"}]}
|
||||
{"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)))
|
||||
?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\"}]}">>,
|
||||
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"},
|
||||
{"tagSet", [
|
||||
{"resourceId", "i-13a4abea"},
|
||||
{"resourceType", "instance"},
|
||||
{"key", "Environment"},
|
||||
{"value", "prod-us-east-1"},
|
||||
|
@ -49,14 +62,25 @@ parse_test_() ->
|
|||
{"resourceId", "i-13a4abea"},
|
||||
{"resourceType", "instance"},
|
||||
{"key", "aws:cloudformation:stack-name"},
|
||||
{"value","prod-us-east-1-ecs-1"}]}
|
||||
{"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)))
|
||||
?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]\}",
|
||||
|
|
|
@ -3,63 +3,82 @@
|
|||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("rabbitmq_aws.hrl").
|
||||
|
||||
|
||||
amz_date_test_() ->
|
||||
[
|
||||
{"value", fun() ->
|
||||
?assertEqual("20160220",
|
||||
rabbitmq_aws_sign:amz_date("20160220T120000Z"))
|
||||
?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"}],
|
||||
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)},
|
||||
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))
|
||||
{"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"}],
|
||||
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)},
|
||||
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))
|
||||
{"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() ->
|
||||
|
@ -68,32 +87,46 @@ authorization_header_test_() ->
|
|||
RequestTimestamp = "20150830T123600Z",
|
||||
Region = "us-east-1",
|
||||
Service = "iam",
|
||||
Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
Headers = [
|
||||
{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"Host", "iam.amazonaws.com"},
|
||||
{"Date", "20150830T123600Z"}],
|
||||
{"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))
|
||||
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"},
|
||||
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"}],
|
||||
{"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"]),
|
||||
"my-header2:\"a b c \"\n"
|
||||
]),
|
||||
?assertEqual(Expectation, rabbitmq_aws_sign:canonical_headers(Value))
|
||||
end}
|
||||
].
|
||||
|
@ -105,30 +138,72 @@ credential_scope_test_() ->
|
|||
Region = "us-east-1",
|
||||
Service = "iam",
|
||||
Expectation = "20150830/us-east-1/iam/aws4_request",
|
||||
?assertEqual(Expectation,
|
||||
rabbitmq_aws_sign:credential_scope(RequestDate, Region, Service))
|
||||
?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"))
|
||||
?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"},
|
||||
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))
|
||||
{"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))
|
||||
|
@ -141,38 +216,80 @@ request_hash_test_() ->
|
|||
Method = get,
|
||||
Path = "/",
|
||||
QArgs = [{"Action", "ListUsers"}, {"Version", "2010-05-08"}],
|
||||
Headers = [{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
Headers = [
|
||||
{"Content-Type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"Host", "iam.amazonaws.com"},
|
||||
{"Date", "20150830T123600Z"}],
|
||||
{"Date", "20150830T123600Z"}
|
||||
],
|
||||
Payload = "",
|
||||
Expectation = "49b454e0f20fe17f437eaa570846fc5d687efc1752c8b5a1eeee5597a7eb92a5",
|
||||
?assertEqual(Expectation,
|
||||
rabbitmq_aws_sign:request_hash(Method, Path, QArgs, Headers, Payload))
|
||||
?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],
|
||||
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"},
|
||||
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",
|
||||
{"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}
|
||||
].
|
||||
|
@ -184,9 +301,44 @@ signing_key_test_() ->
|
|||
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))
|
||||
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}
|
||||
].
|
||||
|
||||
|
@ -198,9 +350,14 @@ string_to_sign_test_() ->
|
|||
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))
|
||||
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}
|
||||
].
|
||||
|
||||
|
@ -214,7 +371,9 @@ local_time_0_test_() ->
|
|||
end,
|
||||
[
|
||||
{"variation1", fun() ->
|
||||
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 05, 08}, {12, 36, 00}}] end),
|
||||
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
|
||||
[{{2015, 05, 08}, {12, 36, 00}}]
|
||||
end),
|
||||
Expectation = "20150508T123600Z",
|
||||
?assertEqual(Expectation, rabbitmq_aws_sign:local_time()),
|
||||
meck:validate(calendar)
|
||||
|
@ -245,7 +404,9 @@ headers_test_() ->
|
|||
end,
|
||||
[
|
||||
{"without signing key", fun() ->
|
||||
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [{{2015, 08, 30}, {12, 36, 00}}] end),
|
||||
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
|
||||
[{{2015, 08, 30}, {12, 36, 00}}]
|
||||
end),
|
||||
Request = #request{
|
||||
access_key = "AKIDEXAMPLE",
|
||||
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
|
@ -254,20 +415,25 @@ headers_test_() ->
|
|||
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"}]},
|
||||
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"},
|
||||
{"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"}
|
||||
{"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),
|
||||
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",
|
||||
|
@ -276,16 +442,18 @@ headers_test_() ->
|
|||
region = "us-east-1",
|
||||
uri = "https://s3.us-east-1.amazonaws.com/?list-type=2",
|
||||
body = "",
|
||||
headers = [{"host", "gavinroy.com.s3.amazonaws.com"}]},
|
||||
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"},
|
||||
{"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"}
|
||||
{"x-amz-content-sha256",
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
|
||||
],
|
||||
?assertEqual(Expectation, rabbitmq_aws_sign:headers(Request)),
|
||||
meck:validate(calendar)
|
||||
end}
|
||||
]
|
||||
}.
|
||||
]}.
|
||||
|
|
|
@ -13,15 +13,19 @@ start_link_test_() ->
|
|||
[
|
||||
{"supervisor start_link", fun() ->
|
||||
meck:expect(supervisor, start_link, fun(_, _, _) -> {ok, test_result} end),
|
||||
?assertEqual({ok, test_result},
|
||||
rabbitmq_aws_sup:start_link()),
|
||||
?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([])
|
||||
).
|
||||
|
|
|
@ -25,7 +25,9 @@ init_test_() ->
|
|||
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},
|
||||
Expectation =
|
||||
{state, "Sésame", "ouvre-toi", undefined, undefined, "us-west-3", undefined,
|
||||
undefined},
|
||||
?assertEqual(Expectation, State)
|
||||
end},
|
||||
{"error", fun() ->
|
||||
|
@ -35,12 +37,13 @@ init_test_() ->
|
|||
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},
|
||||
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)).
|
||||
|
@ -56,7 +59,9 @@ endpoint_test_() ->
|
|||
Path = "/",
|
||||
Host = "localhost:32767",
|
||||
Expectation = "https://localhost:32767/",
|
||||
?assertEqual(Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path))
|
||||
?assertEqual(
|
||||
Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)
|
||||
)
|
||||
end},
|
||||
{"unspecified", fun() ->
|
||||
Region = "us-east-3",
|
||||
|
@ -64,7 +69,9 @@ endpoint_test_() ->
|
|||
Path = "/",
|
||||
Host = undefined,
|
||||
Expectation = "https://dynamodb.us-east-3.amazonaws.com/",
|
||||
?assertEqual(Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path))
|
||||
?assertEqual(
|
||||
Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
|
@ -100,14 +107,18 @@ expired_credentials_test_() ->
|
|||
{"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),
|
||||
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),
|
||||
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},
|
||||
|
@ -120,18 +131,27 @@ expired_credentials_test_() ->
|
|||
format_response_test_() ->
|
||||
[
|
||||
{"ok", fun() ->
|
||||
Response = {ok, {{"HTTP/1.1", 200, "Ok"}, [{"Content-Type", "text/xml"}], "<test>Value</test>"}},
|
||||
Response =
|
||||
{ok, {
|
||||
{"HTTP/1.1", 200, "Ok"}, [{"Content-Type", "text/xml"}], "<test>Value</test>"
|
||||
}},
|
||||
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"}], "<error>Boom</error>"}},
|
||||
Expectation = {error, "Internal Server Error", {[{"Content-Type", "text/xml"}], [{"error", "Boom"}]}},
|
||||
Response =
|
||||
{ok, {
|
||||
{"HTTP/1.1", 500, "Internal Server Error"},
|
||||
[{"Content-Type", "text/xml"}],
|
||||
"<error>Boom</error>"
|
||||
}},
|
||||
Expectation =
|
||||
{error, "Internal Server Error",
|
||||
{[{"Content-Type", "text/xml"}], [{"error", "Boom"}]}},
|
||||
?assertEqual(Expectation, rabbitmq_aws:format_response(Response))
|
||||
end}
|
||||
].
|
||||
|
||||
|
||||
gen_server_call_test_() ->
|
||||
{
|
||||
foreach,
|
||||
|
@ -154,9 +174,11 @@ gen_server_call_test_() ->
|
|||
{
|
||||
"request",
|
||||
fun() ->
|
||||
State = #state{access_key = "AKIDEXAMPLE",
|
||||
State = #state{
|
||||
access_key = "AKIDEXAMPLE",
|
||||
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
region = "us-east-1"},
|
||||
region = "us-east-1"
|
||||
},
|
||||
Service = "ec2",
|
||||
Method = get,
|
||||
Headers = [],
|
||||
|
@ -164,12 +186,29 @@ gen_server_call_test_() ->
|
|||
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),
|
||||
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
|
||||
|
@ -177,34 +216,46 @@ gen_server_call_test_() ->
|
|||
{
|
||||
"get_state",
|
||||
fun() ->
|
||||
State = #state{access_key = "AKIDEXAMPLE",
|
||||
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))
|
||||
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",
|
||||
State = #state{
|
||||
access_key = "AKIDEXAMPLE",
|
||||
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
region = "us-east-1"},
|
||||
State2 = #state{access_key = "AKIDEXAMPLE2",
|
||||
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()},
|
||||
security_token =
|
||||
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L2",
|
||||
expiration = calendar:local_time()
|
||||
},
|
||||
meck:new(rabbitmq_aws_config, [passthrough]),
|
||||
meck:expect(rabbitmq_aws_config, credentials,
|
||||
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)),
|
||||
{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
|
||||
|
@ -212,24 +263,37 @@ gen_server_call_test_() ->
|
|||
{
|
||||
"set_credentials",
|
||||
fun() ->
|
||||
State = #state{access_key = "AKIDEXAMPLE",
|
||||
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"}))
|
||||
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",
|
||||
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"}))
|
||||
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
|
||||
}
|
||||
]
|
||||
|
@ -259,7 +323,6 @@ has_credentials_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
local_time_test_() ->
|
||||
{
|
||||
foreach,
|
||||
|
@ -278,7 +341,6 @@ local_time_test_() ->
|
|||
]
|
||||
}.
|
||||
|
||||
|
||||
maybe_decode_body_test_() ->
|
||||
[
|
||||
{"application/x-amz-json-1.0", fun() ->
|
||||
|
@ -322,7 +384,6 @@ parse_content_type_test_() ->
|
|||
end}
|
||||
].
|
||||
|
||||
|
||||
perform_request_test_() ->
|
||||
{
|
||||
foreach,
|
||||
|
@ -336,9 +397,11 @@ perform_request_test_() ->
|
|||
{
|
||||
"has_credentials true",
|
||||
fun() ->
|
||||
State = #state{access_key = "AKIDEXAMPLE",
|
||||
State = #state{
|
||||
access_key = "AKIDEXAMPLE",
|
||||
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
region = "us-east-1"},
|
||||
region = "us-east-1"
|
||||
},
|
||||
Service = "ec2",
|
||||
Method = get,
|
||||
Headers = [],
|
||||
|
@ -346,21 +409,37 @@ perform_request_test_() ->
|
|||
Body = "",
|
||||
Options = [],
|
||||
Host = undefined,
|
||||
ExpectURI = "https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01",
|
||||
meck:expect(httpc, request,
|
||||
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", 200, "OK"},
|
||||
[{"content-type", "application/json"}],
|
||||
"{\"pass\": true}"
|
||||
}};
|
||||
_ ->
|
||||
{ok, {{"HTTP/1.0", 400, "RequestFailure", [{"content-type", "application/json"}], "{\"pass\": false}"}}}
|
||||
{ok,
|
||||
{{"HTTP/1.0", 400, "RequestFailure",
|
||||
[{"content-type", "application/json"}],
|
||||
"{\"pass\": false}"}}}
|
||||
end
|
||||
end),
|
||||
Expectation = {{ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, State},
|
||||
Result = rabbitmq_aws:perform_request(State, Service, Method, Headers, Path, Body, Options, Host),
|
||||
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},
|
||||
end
|
||||
},
|
||||
{
|
||||
"has_credentials false",
|
||||
fun() ->
|
||||
|
@ -372,9 +451,17 @@ perform_request_test_() ->
|
|||
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),
|
||||
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),
|
||||
Result = rabbitmq_aws:perform_request(
|
||||
State, Service, Method, Headers, Path, Body, Options, Host
|
||||
),
|
||||
?assertEqual(Expectation, Result),
|
||||
meck:validate(httpc)
|
||||
end
|
||||
|
@ -382,11 +469,14 @@ perform_request_test_() ->
|
|||
{
|
||||
"has expired credentials",
|
||||
fun() ->
|
||||
State = #state{access_key = "AKIDEXAMPLE",
|
||||
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}}},
|
||||
security_token =
|
||||
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L",
|
||||
expiration = {{1973, 1, 1}, {10, 20, 30}}
|
||||
},
|
||||
Service = "ec2",
|
||||
Method = get,
|
||||
Headers = [],
|
||||
|
@ -395,8 +485,12 @@ perform_request_test_() ->
|
|||
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),
|
||||
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
|
||||
|
@ -407,7 +501,8 @@ perform_request_test_() ->
|
|||
State = #state{error = unit_test},
|
||||
Expectation = {{error, {credentials, State#state.error}}, State},
|
||||
?assertEqual(Expectation, rabbitmq_aws:perform_request_creds_error(State))
|
||||
end}
|
||||
end
|
||||
}
|
||||
]
|
||||
}.
|
||||
|
||||
|
@ -423,21 +518,33 @@ sign_headers_test_() ->
|
|||
{"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",
|
||||
State = #state{
|
||||
access_key = "AKIDEXAMPLE",
|
||||
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
security_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L",
|
||||
region = "us-east-1"},
|
||||
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"},
|
||||
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)),
|
||||
{"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}
|
||||
]
|
||||
|
@ -453,13 +560,23 @@ api_get_request_test_() ->
|
|||
end,
|
||||
fun meck:unload/1,
|
||||
[
|
||||
{"AWS service API request succeeded",
|
||||
fun() ->
|
||||
State = #state{access_key = "ExpiredKey",
|
||||
{"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\"}"}}),
|
||||
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),
|
||||
|
@ -467,24 +584,22 @@ api_get_request_test_() ->
|
|||
ok = gen_server:stop(Pid),
|
||||
?assertEqual({ok, [{"data", "value"}]}, Result),
|
||||
meck:validate(httpc)
|
||||
end
|
||||
},
|
||||
{"AWS service API request failed - credentials",
|
||||
fun() ->
|
||||
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",
|
||||
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}}},
|
||||
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"),
|
||||
|
@ -493,19 +608,32 @@ api_get_request_test_() ->
|
|||
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",
|
||||
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([
|
||||
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, {
|
||||
{"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),
|
||||
|
@ -513,8 +641,7 @@ api_get_request_test_() ->
|
|||
ok = gen_server:stop(Pid),
|
||||
?assertEqual({ok, [{"data", "value"}]}, Result),
|
||||
meck:validate(httpc)
|
||||
end
|
||||
}
|
||||
end}
|
||||
]
|
||||
}.
|
||||
|
||||
|
@ -527,25 +654,28 @@ ensure_credentials_valid_test_() ->
|
|||
end,
|
||||
fun meck:unload/1,
|
||||
[
|
||||
{"expired credentials are refreshed",
|
||||
fun() ->
|
||||
State = #state{access_key = "ExpiredKey",
|
||||
{"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",
|
||||
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}}},
|
||||
expiration = {{3016, 4, 1}, {12, 0, 0}}
|
||||
},
|
||||
|
||||
meck:expect(rabbitmq_aws_config, credentials,
|
||||
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, 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),
|
||||
|
@ -556,12 +686,13 @@ ensure_credentials_valid_test_() ->
|
|||
?assertEqual(Credentials, {ok, State2}),
|
||||
meck:validate(rabbitmq_aws_config)
|
||||
end},
|
||||
{"valid credentials are returned",
|
||||
fun() ->
|
||||
State = #state{access_key = "GoodKey",
|
||||
{"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}}},
|
||||
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),
|
||||
|
@ -572,20 +703,21 @@ ensure_credentials_valid_test_() ->
|
|||
?assertEqual(Credentials, {ok, State}),
|
||||
meck:validate(rabbitmq_aws_config)
|
||||
end},
|
||||
{"load credentials if missing",
|
||||
fun() ->
|
||||
State = #state{access_key = "GoodKey",
|
||||
{"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,
|
||||
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, 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(),
|
||||
|
@ -600,31 +732,23 @@ ensure_credentials_valid_test_() ->
|
|||
|
||||
expired_imdsv2_token_test_() ->
|
||||
[
|
||||
{"imdsv2 token is valid",
|
||||
fun() ->
|
||||
{"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() ->
|
||||
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() ->
|
||||
end},
|
||||
{"imdsv2 token is not yet initialized", fun() ->
|
||||
?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(undefined))
|
||||
end
|
||||
},
|
||||
{"imdsv2 token is undefined",
|
||||
fun() ->
|
||||
end},
|
||||
{"imdsv2 token is undefined", fun() ->
|
||||
Imdsv2Token = #imdsv2token{token = undefined, expiration = undefined},
|
||||
?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token))
|
||||
end
|
||||
}
|
||||
end}
|
||||
].
|
||||
|
|
|
@ -8,147 +8,178 @@ build_test_() ->
|
|||
[
|
||||
{"variation1", fun() ->
|
||||
Expect = "amqp://guest:password@rabbitmq:5672/%2F?heartbeat=5",
|
||||
Value = #uri{scheme = "amqp",
|
||||
Value = #uri{
|
||||
scheme = "amqp",
|
||||
authority = {{"guest", "password"}, "rabbitmq", 5672},
|
||||
path = "/%2F", query = [{"heartbeat", "5"}]},
|
||||
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,
|
||||
Value = #uri{
|
||||
scheme = http,
|
||||
authority = {undefined, "www.google.com", 80},
|
||||
path = "/search",
|
||||
query = [{"foo", "bar"}],
|
||||
fragment = "baz"},
|
||||
fragment = "baz"
|
||||
},
|
||||
Result = rabbitmq_aws_urilib:build(Value),
|
||||
?assertEqual(Expect, Result)
|
||||
end},
|
||||
{"variation3", fun() ->
|
||||
Expect = "https://www.google.com/search",
|
||||
Value = #uri{scheme = "https",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {undefined, "www.google.com", undefined},
|
||||
path = "/search"},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {undefined, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", true}]},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {{"bar", undefined}, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", true}]},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {undefined, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", true}]},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {{"", ""}, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", true}]},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "https",
|
||||
authority = {{"bar", ""}, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", true}],
|
||||
fragment=""},
|
||||
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",
|
||||
Value = #uri{
|
||||
scheme = "http",
|
||||
authority = {undefined, "www.google.com", undefined},
|
||||
path = "/search",
|
||||
query = [{"foo", true}],
|
||||
fragment = "bar"},
|
||||
fragment = "bar"
|
||||
},
|
||||
Result = rabbitmq_aws_urilib:build(Value),
|
||||
?assertEqual(Expect, Result)
|
||||
end},
|
||||
{"variation11", fun() ->
|
||||
Expect = "http://www.google.com",
|
||||
Value = #uri{scheme = "http",
|
||||
Value = #uri{
|
||||
scheme = "http",
|
||||
authority = {undefined, "www.google.com", undefined},
|
||||
path = undefined,
|
||||
query = []},
|
||||
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"}]))
|
||||
?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",
|
||||
Expect = #uri{
|
||||
scheme = "amqp",
|
||||
authority = {{"guest", "password"}, "rabbitmq", 5672},
|
||||
path = "/%2F",
|
||||
query = [{"heartbeat", "5"}],
|
||||
fragment = undefined},
|
||||
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",
|
||||
Expect = #uri{
|
||||
scheme = "http",
|
||||
authority = {undefined, "www.google.com", 80},
|
||||
path = "/search",
|
||||
query = [{"foo", "bar"}],
|
||||
fragment = "baz"},
|
||||
fragment = "baz"
|
||||
},
|
||||
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
|
||||
end},
|
||||
{"variation3", fun() ->
|
||||
URI = "https://www.google.com/search",
|
||||
Expect = #uri{scheme = "https",
|
||||
Expect = #uri{
|
||||
scheme = "https",
|
||||
authority = {undefined, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = "",
|
||||
fragment = undefined},
|
||||
fragment = undefined
|
||||
},
|
||||
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
|
||||
end},
|
||||
{"variation4", fun() ->
|
||||
URI = "https://www.google.com/search?foo=true",
|
||||
Expect = #uri{scheme = "https",
|
||||
Expect = #uri{
|
||||
scheme = "https",
|
||||
authority = {undefined, "www.google.com", 443},
|
||||
path = "/search",
|
||||
query = [{"foo", "true"}],
|
||||
fragment = undefined},
|
||||
fragment = undefined
|
||||
},
|
||||
?assertEqual(Expect, rabbitmq_aws_urilib:parse(URI))
|
||||
end}
|
||||
].
|
||||
|
|
|
@ -5,28 +5,40 @@
|
|||
parse_test_() ->
|
||||
[
|
||||
{"s3 error response", fun() ->
|
||||
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIAIPPU25E5RA4MIYKQ</AWSAccessKeyId><StringToSign>AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7</StringToSign><SignatureProvided>841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e</SignatureProvided><StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37</StringToSignBytes><CanonicalRequest>GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</CanonicalRequest><CanonicalRequestBytes>47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35</CanonicalRequestBytes><RequestId>8EB36F450B78C45D</RequestId><HostId>IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=</HostId></Error>",
|
||||
Expectation = [{"Error", [
|
||||
Response =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIAIPPU25E5RA4MIYKQ</AWSAccessKeyId><StringToSign>AWS4-HMAC-SHA256\n20160516T041429Z\n20160516/us-east-1/s3/aws4_request\n7e908e36ea6c07e542ffac21ec3e11acc3baf022d9133d9764e1521b152586f7</StringToSign><SignatureProvided>841d7b89150d246feee9bceb90f5cae91d0c45f44851742c73eb87dc8472748e</SignatureProvided><StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 32 30 31 36 30 35 31 36 2f 75 73 2d 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 37 65 39 30 38 65 33 36 65 61 36 63 30 37 65 35 34 32 66 66 61 63 32 31 65 63 33 65 31 31 61 63 63 33 62 61 66 30 32 32 64 39 31 33 33 64 39 37 36 34 65 31 35 32 31 62 31 35 32 35 38 36 66 37</StringToSignBytes><CanonicalRequest>GET\n/\nlist-type=2\ncontent-length:0\ndate:20160516T041429Z\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\ncontent-length;date;host;x-amz-content-sha256\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</CanonicalRequest><CanonicalRequestBytes>47 45 54 0a 2f 0a 6c 69 73 74 2d 74 79 70 65 3d 32 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 30 0a 64 61 74 65 3a 32 30 31 36 30 35 31 36 54 30 34 31 34 32 39 5a 0a 68 6f 73 74 3a 73 33 2e 75 73 2d 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35 0a 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3b 64 61 74 65 3b 68 6f 73 74 3b 78 2d 61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 0a 65 33 62 30 63 34 34 32 39 38 66 63 31 63 31 34 39 61 66 62 66 34 63 38 39 39 36 66 62 39 32 34 32 37 61 65 34 31 65 34 36 34 39 62 39 33 34 63 61 34 39 35 39 39 31 62 37 38 35 32 62 38 35 35</CanonicalRequestBytes><RequestId>8EB36F450B78C45D</RequestId><HostId>IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw=</HostId></Error>",
|
||||
Expectation = [
|
||||
{"Error", [
|
||||
{"Code", "SignatureDoesNotMatch"},
|
||||
{"Message", "The request signature we calculated does not match the signature you provided. Check your key and signing method."},
|
||||
{"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"},
|
||||
{"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="}
|
||||
]}],
|
||||
{"HostId",
|
||||
"IYXsnJ59yqGI/IzjGoPGUz7NGb/t0ETlWH4v5+l8EGWmHLbhB1b2MsjbSaY5A8M3g7Fn/Nliqpw="}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
|
||||
end},
|
||||
{"whitespace", fun() ->
|
||||
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test> <example> value</example>\n</test> \n",
|
||||
Response =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test> <example> value</example>\n</test> \n",
|
||||
Expectation = [{"test", [{"example", "value"}]}],
|
||||
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
|
||||
end},
|
||||
{"multiple items", fun() ->
|
||||
Response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test><values><example>value</example><example>value2</example></values>\n</test> \n",
|
||||
Response =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test><values><example>value</example><example>value2</example></values>\n</test> \n",
|
||||
Expectation = [{"test", [{"values", [{"example", "value"}, {"example", "value2"}]}]}],
|
||||
?assertEqual(Expectation, rabbitmq_aws_xml:parse(Response))
|
||||
end},
|
||||
|
|
Loading…
Reference in New Issue