From 91295945bbdec5fc84b5a2620768c0450b08da7f Mon Sep 17 00:00:00 2001 From: Brujo Benavides Date: Thu, 16 Apr 2020 09:17:51 +0200 Subject: [PATCH] Add rebar3 formatting (#5) * Add rebar3 formatting * Remove old way * Remove lint warnings * Actually verify the formatting on CI * Remove support for OTP 20 * Revert indentation of long lists * Moving comments until AdRoll/rebar3_format#83 is fixed --- .travis.yml | 4 +- elvis.config | 9 +- include/erliam.hrl | 11 +- rebar.config | 6 +- src/awsv4.erl | 392 +++++++++++++++++++++--------------------- src/erliam.erl | 21 +-- src/erliam_config.erl | 14 +- src/erliam_srv.erl | 97 +++++------ src/erliam_sts.erl | 48 +++--- src/erliam_sup.erl | 8 +- src/erliam_util.erl | 46 +++-- src/erliam_xml.erl | 71 ++++---- src/imds.erl | 181 +++++++++---------- 13 files changed, 422 insertions(+), 486 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19bd941..4f81bed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ os: - linux otp_release: - - 20.3 - 21.3 notifications: @@ -15,6 +14,5 @@ script: - export PATH=/home/travis/.cache/rebar3/bin:$PATH - rebar3 --version - erl -version + - rebar3 format --verify - rebar3 test - - git --no-pager diff - - if [[ $(git --no-pager diff) ]]; then echo "Your code is not properly formatted, run rebar3 format on it"; exit 999; fi diff --git a/elvis.config b/elvis.config index d908358..9bcd680 100644 --- a/elvis.config +++ b/elvis.config @@ -7,14 +7,9 @@ filter => "*.erl", ruleset => erl_files, rules => [ - {elvis_style, line_length, #{limit => 100}}, + {elvis_style, line_length, #{limit => 120}}, {elvis_style, nesting_level, #{level => 4}}, - %% the default rule included {right, ","} and not {right, "=>"} or {left, "=>"} - { - elvis_style, - operator_spaces, - #{rules => [{right, "++"}, {left, "++"}, {right, "=>"}, {left, "=>"}]} - } + {elvis_style, dont_repeat_yourself, #{min_complexity => 15}} ] }, #{dirs => ["."], diff --git a/include/erliam.hrl b/include/erliam.hrl index 55ae24e..201ddaa 100644 --- a/include/erliam.hrl +++ b/include/erliam.hrl @@ -1,9 +1,8 @@ -type aws_datetime() :: string(). % "YYYYMMDDTHHMMSSZ" -type iso_datetime() :: string(). % "YYYY-MM-DDTHH:MM:SSZ" --record(credentials, { - expiration :: undefined | iso_datetime(), - security_token :: undefined | string(), % required when using temporary credentials - secret_access_key :: string(), - access_key_id :: string() - }). +-record(credentials, + {expiration :: undefined | iso_datetime(), + security_token :: undefined | string(), % required when using temporary credentials + secret_access_key :: string(), + access_key_id :: string()}). diff --git a/rebar.config b/rebar.config index ea58813..aa295b6 100644 --- a/rebar.config +++ b/rebar.config @@ -39,6 +39,8 @@ deprecated_functions ]}. -{alias, [{test, [xref, dialyzer, lint, eunit, cover]}]}. +{alias, [{test, [format, lint, xref, dialyzer, eunit, cover]}]}. -{plugins, [{rebar3_lint, "0.1.10"}]}. +{plugins, [rebar3_lint, rebar3_format]}. + +{format, [{files, ["src/*.erl", "include/*.hrl"]}]}. diff --git a/src/awsv4.erl b/src/awsv4.erl index 0a166ad..0e661fc 100644 --- a/src/awsv4.erl +++ b/src/awsv4.erl @@ -7,21 +7,23 @@ %%% Created : 1 Feb 2017 by Mike Watters -module(awsv4). --export([headers/2, headers/3, headers/11, +-export([headers/2, + headers/3, + headers/11, canonical_query/1, long_term_credentials/2, credentials_from_plist/1, - isonow/0, isonow/1]). - + isonow/0, + isonow/1]). -include("erliam.hrl"). -type credentials() :: #credentials{}. + -export_type([credentials/0]). -type pairs() :: #{string() => iodata()} | [{string(), iodata()}]. - %%%% API headers(Credentials, Parameters) -> @@ -29,9 +31,9 @@ headers(Credentials, Parameters) -> headers(Credentials, Parameters, undefined) -> headers(Credentials, Parameters, <<>>); - -headers(Credentials, #{service := Service, - region := Region} = Parameters, RequestPayload) -> +headers(Credentials, + #{service := Service, region := Region} = Parameters, + RequestPayload) -> Defaults = #{target_api => undefined, method => "GET", path => "/", @@ -42,27 +44,34 @@ headers(Credentials, #{service := Service, headers_(Credentials, maps:merge(Defaults, Parameters), RequestPayload). -spec headers(Credentials :: credentials(), - Service :: string(), % e.g., "s3" - Region :: string(), % e.g., "us-west-2" - Host :: string(), % e.g., "bucketname.s3.amazonaws.com" - AwsDate :: undefined | aws_datetime(), % e.g., "20170101T000000Z" - TargetAPI :: string() | undefined, % e.g., "DynamoDB_20120810.CreateTable" - Method :: string(), % e.g., "GET", "POST" - Path :: string(), + Service :: string(), + Region :: string(), + Host :: string(), + AwsDate :: undefined | aws_datetime(), + TargetAPI :: string() | undefined, + Method :: string(), + Path :: string(), QueryParams :: pairs(), ExtraSignedHeaders :: pairs(), - RequestPayload :: binary()) -> - [{HeaderName :: string(), HeaderValue :: iodata()}]. + RequestPayload :: binary()) -> [{HeaderName :: string(), HeaderValue :: iodata()}]. headers(#credentials{secret_access_key = SecretAccessKey, access_key_id = AccessKeyId, security_token = SecurityToken}, - Service, Region, Host, AwsDate, TargetAPI, - Method, Path, QueryParams, ExtraSignedHeaders, + Service, + Region, + Host, + AwsDate, + TargetAPI, + Method, + Path, + QueryParams, + ExtraSignedHeaders, RequestPayload) -> - ActualAwsDate = case AwsDate of - undefined -> isonow(); - _ -> AwsDate + undefined -> + isonow(); + _ -> + AwsDate end, Date = lists:sublist(ActualAwsDate, 8), % yyyymmdd Scope = [Date, Region, Service, "aws4_request"], @@ -75,108 +84,115 @@ headers(#credentials{secret_access_key = SecretAccessKey, "AWS4" ++ SecretAccessKey, Scope), - Headers = lists:keysort(1, [{string:to_lower(K), V} - || {K, V} <- [{"host", Host}, - {"x-amz-date", ActualAwsDate}, - {"x-amz-security-token", SecurityToken}, - {"x-amz-target", TargetAPI}] - ++ if - is_map(ExtraSignedHeaders) -> - maps:to_list(ExtraSignedHeaders); - true -> - ExtraSignedHeaders - end, - V /= undefined]), + Headers = lists:keysort(1, + [{string:to_lower(K), V} + || {K, V} + <- [{"host", Host}, + {"x-amz-date", ActualAwsDate}, + {"x-amz-security-token", SecurityToken}, + {"x-amz-target", TargetAPI}] + ++ + if is_map(ExtraSignedHeaders) -> + maps:to_list(ExtraSignedHeaders); + true -> + ExtraSignedHeaders + end, + V /= undefined]), SignedHeaders = string:join([Name || {Name, _} <- Headers], ";"), CanonicalHeaders = [[K, $:, V, $\n] || {K, V} <- Headers], PayloadHash = hexlify(crypto:hash(Hash, RequestPayload)), - CanonicalRequest = join($\n, [Method, - canonical_path(Service, Path), - canonical_query(QueryParams), - CanonicalHeaders, - SignedHeaders, - PayloadHash]), + CanonicalRequest = join($\n, + [Method, + canonical_path(Service, Path), + canonical_query(QueryParams), + CanonicalHeaders, + SignedHeaders, + PayloadHash]), CredentialScope = join($/, Scope), - StringToSign = join($\n, [Algorithm, - ActualAwsDate, - CredentialScope, - hexlify(crypto:hash(Hash, CanonicalRequest))]), + StringToSign = join($\n, + [Algorithm, + ActualAwsDate, + CredentialScope, + hexlify(crypto:hash(Hash, CanonicalRequest))]), Signature = hexlify(crypto:hmac(Hash, SigningKey, StringToSign)), [{"authorization", [Algorithm, - " Credential=", AccessKeyId, $/, CredentialScope, - ",SignedHeaders=", SignedHeaders, - ",Signature=", Signature]}, + " Credential=", + AccessKeyId, + $/, + CredentialScope, + ",SignedHeaders=", + SignedHeaders, + ",Signature=", + Signature]}, {"x-amz-content-sha256", PayloadHash} | Headers]. - -spec isonow(calendar:datetime()) -> aws_datetime(). isonow({{Year, Month, Day}, {Hour, Min, Sec}}) -> lists:flatten(io_lib:format("~4.10.0B~2.10.0B~2.10.0BT~2.10.0B~2.10.0B~2.10.0BZ", [Year, Month, Day, Hour, Min, Sec])). + isonow() -> isonow(calendar:universal_time()). - %% fixme; handle repeated params. -spec canonical_query(pairs()) -> iolist(). canonical_query(QueryParams) when is_list(QueryParams) -> - join($&, [[quote(K), $=, quote(V)] - || {K, V} <- lists:keysort(1, QueryParams), - V /= undefined]); + join($&, + [[quote(K), $=, quote(V)] || {K, V} <- lists:keysort(1, QueryParams), V /= undefined]); canonical_query(QueryParams) when is_map(QueryParams) -> canonical_query(maps:to_list(QueryParams)). - -spec long_term_credentials(iodata(), iodata()) -> credentials(). long_term_credentials(AccessKeyId, SecretAccessKey) -> - #credentials{access_key_id = AccessKeyId, - secret_access_key = SecretAccessKey}. - + #credentials{access_key_id = AccessKeyId, secret_access_key = SecretAccessKey}. --spec credentials_from_plist(list({expiration | token | access_key_id | secret_access_key, - iodata() | undefined})) -> credentials(). +-spec credentials_from_plist([{expiration | token | access_key_id | secret_access_key, + iodata() | undefined}]) -> credentials(). credentials_from_plist(Plist) -> - #credentials{ - expiration = erliam_util:getkey(expiration, Plist), - security_token = erliam_util:getkey(token, Plist), - access_key_id = erliam_util:getkey(access_key_id, Plist), - secret_access_key = erliam_util:getkey(secret_access_key, Plist) - }. - + #credentials{expiration = erliam_util:getkey(expiration, Plist), + security_token = erliam_util:getkey(token, Plist), + access_key_id = erliam_util:getkey(access_key_id, Plist), + secret_access_key = erliam_util:getkey(secret_access_key, Plist)}. %%%% INTERNAL FUNCTIONS -headers_(Credentials, #{service := Service, - region := Region, - host := Host, - target_api := TargetAPI, - aws_date := AwsDate, - method := Method, - path := Path, - query_params := QueryParams, - signed_headers := ExtraSignedHeaders}, +headers_(Credentials, + #{service := Service, + region := Region, + host := Host, + target_api := TargetAPI, + aws_date := AwsDate, + method := Method, + path := Path, + query_params := QueryParams, + signed_headers := ExtraSignedHeaders}, RequestPayload) -> headers(Credentials, - Service, Region, Host, AwsDate, TargetAPI, - Method, Path, QueryParams, ExtraSignedHeaders, + Service, + Region, + Host, + AwsDate, + TargetAPI, + Method, + Path, + QueryParams, + ExtraSignedHeaders, RequestPayload). - canonical_path(_Service, Path) -> %% note: should remove redundant and relative path components, except leave empty path %% components for s3. quote(Path, path). - quote(X) -> quote(X, all). @@ -185,32 +201,30 @@ quote(undefined, _) -> quote(X, Kind) when is_list(X) -> quote(unicode:characters_to_binary(X, utf8), Kind); quote(X, Kind) when is_binary(X) -> - << <<(case should_encode(C, Kind) of - true -> - [A, B] = string:to_upper(int_to_hex(C)), - <<"%", A, B>>; - false -> - <> - end)/binary>> - || <> <= X >>. - + << < + [A, B] = string:to_upper(int_to_hex(C)), + <<"%", A, B>>; + false -> + <> + end/binary>> + || <> <= X >>. should_encode(X, Kind) -> %% note: does not encode gen-delims () - X > 127 - orelse X < 33 - orelse lists:member(X, "!$&'()*+,;=" ++ - case Kind of - all -> - ":/?#[]@"; - path -> - [] - end). - + X > 127 orelse + X < 33 orelse + lists:member(X, + "!$&'()*+,;=" ++ + case Kind of + all -> + ":/?#[]@"; + path -> + [] + end). hexlify(Bin) -> - [int_to_hex(X) - || X <- binary_to_list(Bin)]. + [int_to_hex(X) || X <- binary_to_list(Bin)]. int_to_hex(N) when N < 256 -> [hex(N div 16), hex(N rem 16)]. @@ -220,142 +234,126 @@ hex(N) when N < 10 -> hex(N) when N >= 10, N < 16 -> $a + (N - 10). - join(Sep, List) -> lists:foldr(fun (E, []) -> [E]; (E, A) -> [E, Sep | A] - end, [], List). - + end, + [], + List). %%%% TESTS -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +-include_lib("eunit/include/eunit.hrl"). join_test() -> ?assertEqual([], join(y, [])), ?assertEqual([x], join(y, [x])), - ?assertEqual([w,y,x,y,z], join(y, [w,x,z])). - + ?assertEqual([w, y, x, y, z], join(y, [w, x, z])). flattened(KVs) -> - [{K, lists:flatten(V)} - || {K, V} <- KVs]. + [{K, lists:flatten(V)} || {K, V} <- KVs]. flattened_headers(Args) -> flattened(apply(?MODULE, headers, Args)). - basic_headers_test() -> Actual = flattened_headers([#credentials{secret_access_key = "secretkey", access_key_id = "accesskey", security_token = "securitytoken"}, - "kinesis", "us-east-1", "kinesis.us-east-1.amazonaws.com", - "20140629T022822Z", "Kinesis_20131202.ListStreams", - "POST", "/", [], #{}, + "kinesis", + "us-east-1", + "kinesis.us-east-1.amazonaws.com", + "20140629T022822Z", + "Kinesis_20131202.ListStreams", + "POST", + "/", + [], + #{}, "something"]), - Expected = flattened([ - {"authorization", [ - "AWS4-HMAC-SHA256 Credential=accesskey/20140629/us-east-1/kinesis/aws4_request", - ",SignedHeaders=host;x-amz-date;x-amz-security-token;x-amz-target", - ",Signature=847fee48568298911772356fe332443bf2679c48fd42695a84aaa0d0e7f28c66" - ]}, - {"x-amz-content-sha256", - "3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb"}, - {"host","kinesis.us-east-1.amazonaws.com"}, - {"x-amz-date","20140629T022822Z"}, - {"x-amz-security-token","securitytoken"}, - {"x-amz-target","Kinesis_20131202.ListStreams"} - ]), + Expected = flattened([{"authorization", + ["AWS4-HMAC-SHA256 Credential=accesskey/20140629/us-east-1/kinesis/aws" + "4_request", + ",SignedHeaders=host;x-amz-date;x-amz-security-token;x-amz-target", + ",Signature=847fee48568298911772356fe332443bf2679c48fd42695a84aaa0d0e" + "7f28c66"]}, + {"x-amz-content-sha256", + "3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb"}, + {"host", "kinesis.us-east-1.amazonaws.com"}, + {"x-amz-date", "20140629T022822Z"}, + {"x-amz-security-token", "securitytoken"}, + {"x-amz-target", "Kinesis_20131202.ListStreams"}]), ?assertEqual(Expected, Actual). aws4_example1_test() -> %% get-vanilla-query-order-key-case from aws4 test suite - Actual = flattened_headers([ - #credentials{ - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE" - }, - #{ - aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - query_params => #{"Param2" => "value2", "Param1" => "value1"} - } - ]), - Expected = flattened([ - {"authorization", [ - "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request", - ",SignedHeaders=host;x-amz-date", - ",Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500" - ]}, - {"x-amz-content-sha256", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, - {"host", "example.amazonaws.com"}, - {"x-amz-date", "20150830T123600Z"} - ]), + Actual = flattened_headers([#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + query_params => #{"Param2" => "value2", "Param1" => "value1"}}]), + Expected = flattened([{"authorization", + ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" + "ws4_request", + ",SignedHeaders=host;x-amz-date", + ",Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94" + "cdf2500"]}, + {"x-amz-content-sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"host", "example.amazonaws.com"}, + {"x-amz-date", "20150830T123600Z"}]), ?assertEqual(Expected, Actual). aws4_example2_test() -> %% post-vanilla-query from aws4 test suite - Actual = flattened_headers([ - #credentials{ - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE" - }, - #{ - aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - method => "POST", - query_params => #{"Param1" => "value1"} - } - ]), - Expected = flattened([ - {"authorization", [ - "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request", - ",SignedHeaders=host;x-amz-date", - ",Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11" - ]}, - {"x-amz-content-sha256", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, - {"host", "example.amazonaws.com"}, - {"x-amz-date", "20150830T123600Z"} - ]), + Actual = flattened_headers([#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + method => "POST", + query_params => #{"Param1" => "value1"}}]), + Expected = flattened([{"authorization", + ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" + "ws4_request", + ",SignedHeaders=host;x-amz-date", + ",Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d13" + "8af7f11"]}, + {"x-amz-content-sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"host", "example.amazonaws.com"}, + {"x-amz-date", "20150830T123600Z"}]), ?assertEqual(Expected, Actual). aws4_example3_test() -> %% get-unreserved from aws4 test suite - Actual = flattened_headers([ - #credentials{ - secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE" - }, - #{ - aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - path => "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - } - ]), - Expected = flattened([ - {"authorization", [ - "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request", - ",SignedHeaders=host;x-amz-date", - ",Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f" - ]}, - {"x-amz-content-sha256", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, - {"host", "example.amazonaws.com"}, - {"x-amz-date", "20150830T123600Z"} - ]), + Actual = flattened_headers([#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + path => + "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"}]), + Expected = flattened([{"authorization", + ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" + "ws4_request", + ",SignedHeaders=host;x-amz-date", + ",Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65b" + "bf5f24f"]}, + {"x-amz-content-sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"host", "example.amazonaws.com"}, + {"x-amz-date", "20150830T123600Z"}]), ?assertEqual(Expected, Actual). - -endif. diff --git a/src/erliam.erl b/src/erliam.erl index fa4fafa..c8f507b 100644 --- a/src/erliam.erl +++ b/src/erliam.erl @@ -11,37 +11,28 @@ %%% Created : 7 Apr 2017 by Mike Watters -module(erliam). - --export([httpc_profile/0, - get_session_token/0, - credentials/0, - invalidate/0]). - +-export([httpc_profile/0, get_session_token/0, credentials/0, invalidate/0]). %% Return the current cached credentials (crash if none are cached or credential refresher %% server isn't running). credentials() -> erliam_srv:current(). - %% Fetch a new session token from STS (if aws_access_key and aws_secret_key app env values %% are set), or from instance metadata if not. Normally external users don't need to call %% this; call credentials/0 instead. get_session_token() -> case {erliam_config:g(aws_access_key), erliam_config:g(aws_secret_key)} of - {AccessKeyId, SecretAccessKey} when AccessKeyId /= undefined; - SecretAccessKey /= undefined -> - erliam_sts:get_session_token(awsv4:long_term_credentials(AccessKeyId, - SecretAccessKey)); - _ -> - imds:get_session_token() + {AccessKeyId, SecretAccessKey} + when AccessKeyId /= undefined; SecretAccessKey /= undefined -> + erliam_sts:get_session_token(awsv4:long_term_credentials(AccessKeyId, SecretAccessKey)); + _ -> + imds:get_session_token() end. - httpc_profile() -> erliam_config:g(httpc_profile, erliam). - %% force cached credentials to be invalidated and refreshed. invalidate() -> erliam_srv:invalidate(). diff --git a/src/erliam_config.erl b/src/erliam_config.erl index 1ca4502..417e168 100644 --- a/src/erliam_config.erl +++ b/src/erliam_config.erl @@ -2,17 +2,15 @@ -export([g/1, g/2]). - g(Key) -> g(Key, undefined). - g(Key, Default) -> case application:get_env(erliam, Key) of - undefined -> - Default; - {ok, undefined} -> - Default; - {ok, Value} -> - Value + undefined -> + Default; + {ok, undefined} -> + Default; + {ok, Value} -> + Value end. diff --git a/src/erliam_srv.erl b/src/erliam_srv.erl index 026dda0..cbd9d12 100644 --- a/src/erliam_srv.erl +++ b/src/erliam_srv.erl @@ -11,14 +11,17 @@ -behaviour(gen_server). -%% API --export([start_link/0, - current/0, - invalidate/0]). +-format #{inline_items => {when_over, 19}}. +%% API +-export([start_link/0, current/0, invalidate/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -define(SERVER, ?MODULE). -define(TAB, ?MODULE). @@ -28,38 +31,33 @@ -record(state, {}). - %%%% API start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - current() -> [Credentials] = ets:lookup(?TAB, credentials), Credentials. - invalidate() -> gen_server:call(?SERVER, invalidate). - %%%% CALLBACKS init([]) -> ets:new(?TAB, [named_table, public, {read_concurrency, true}]), case update_credentials() of - {error, Error} -> - {stop, Error}; - ok -> - timer:send_interval(1000, refresh), - {ok, #state{}} + {error, Error} -> + {stop, Error}; + ok -> + timer:send_interval(1000, refresh), + {ok, #state{}} end. handle_call(invalidate, _From, State) -> update_credentials(), {reply, ok, State}; - handle_call(_Request, _From, State) -> {reply, {error, not_implemented}, State}. @@ -69,7 +67,6 @@ handle_cast(_Msg, State) -> handle_info(refresh, State) -> maybe_update_credentials(), {noreply, State}; - handle_info(_, State) -> {noreply, State}. @@ -80,55 +77,51 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. - %%%% INTERNAL FUNCTIONS maybe_update_credentials() -> case ets:lookup(?TAB, credentials) of - [Credentials] -> - MinLifetime = ?MIN_LIFETIME, - case remaining_lifetime(Credentials) of - N when N =< MinLifetime -> - update_credentials(); - _ -> - ok - end; - [] -> - update_credentials() + [Credentials] -> + MinLifetime = ?MIN_LIFETIME, + case remaining_lifetime(Credentials) of + N when N =< MinLifetime -> + update_credentials(); + _ -> + ok + end; + [] -> + update_credentials() end. - update_credentials() -> case erliam:get_session_token() of - #credentials{} = Credentials -> - ets:insert(?TAB, Credentials), - ok; - Error -> - error_logger:error_msg("failed to obtain session token: ~p", [Error]), - Error + #credentials{} = Credentials -> + ets:insert(?TAB, Credentials), + ok; + Error -> + error_logger:error_msg("failed to obtain session token: ~p", [Error]), + Error end. - remaining_lifetime(#credentials{expiration = ExpTime}) -> - max(0, calendar:datetime_to_gregorian_seconds(parse_exptime(ExpTime)) - - calendar:datetime_to_gregorian_seconds(calendar:universal_time())). - - -parse_exptime([Y1,Y2,Y3,Y4, $-, Mon1,Mon2, $-, D1,D2, $T, H1,H2, $:, Min1,Min2, $:, S1,S2, $Z]) -> - {{list_to_integer([Y1,Y2,Y3,Y4]), - list_to_integer([Mon1,Mon2]), - list_to_integer([D1,D2])}, - {list_to_integer([H1,H2]), - list_to_integer([Min1,Min2]), - list_to_integer([S1,S2])}}; -parse_exptime( - [Y1,Y2,Y3,Y4, $-, Mon1,Mon2, $-, D1,D2, $T, H1,H2, $:, Min1,Min2, $:, S1,S2, $.,_,_,_, $Z]) -> - parse_exptime([Y1,Y2,Y3,Y4, $-, Mon1,Mon2, $-, D1,D2, $T, H1,H2, $:, Min1,Min2, $:, S1,S2, $Z]). - - + max(0, + calendar:datetime_to_gregorian_seconds(parse_exptime(ExpTime)) - + calendar:datetime_to_gregorian_seconds(calendar:universal_time())). + +parse_exptime([Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, $T, H1, H2, $:, Min1, Min2, $:, + S1, S2, $Z]) -> + {{list_to_integer([Y1, Y2, Y3, Y4]), + list_to_integer([Mon1, Mon2]), + list_to_integer([D1, D2])}, + {list_to_integer([H1, H2]), list_to_integer([Min1, Min2]), list_to_integer([S1, S2])}}; +parse_exptime([Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, $T, H1, H2, $:, Min1, Min2, $:, + S1, S2, $., _, _, _, $Z]) -> + parse_exptime([Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, $T, H1, H2, $:, Min1, Min2, $:, + S1, S2, $Z]). %%%% TESTS -ifdef(TEST). + -include_lib("eunit/include/eunit.hrl"). exptime_test() -> diff --git a/src/erliam_sts.erl b/src/erliam_sts.erl index 19f1a83..32816b4 100644 --- a/src/erliam_sts.erl +++ b/src/erliam_sts.erl @@ -14,65 +14,61 @@ -define(STS_REGION, erliam_config:g(sts_region, "us-east-1")). -define(STS_TIMEOUT, 30000). - %%%% API get_session_token(Credentials) -> - Query = #{"Action" => "GetSessionToken", - "Version" => "2011-06-15"}, + Query = #{"Action" => "GetSessionToken", "Version" => "2011-06-15"}, Host = ?STS_HOST, - Headers = [{"Accept", "text/xml"}] - ++ awsv4:headers(Credentials, #{service => "sts", - region => ?STS_REGION, - query_params => Query, - host => Host}), + Headers = [{"Accept", "text/xml"}] ++ + awsv4:headers(Credentials, + #{service => "sts", + region => ?STS_REGION, + query_params => Query, + host => Host}), Url = "https://" ++ Host ++ "?" ++ awsv4:canonical_query(Query), - decode_response(httpc:request(get, {Url, Headers}, + decode_response(httpc:request(get, + {Url, Headers}, [{timeout, ?STS_TIMEOUT}], [{body_format, binary}], erliam:httpc_profile())). - %%%% INTERNAL FUNCTIONS decode_response({ok, {{_, 200, _}, Headers, Body}}) -> case erliam_util:mime_type(Headers) of - "text/xml" -> - decode_credentials([erliam_xml:parse(Body)]); - _ -> - {error, unacceptable_response} + "text/xml" -> + decode_credentials([erliam_xml:parse(Body)]); + _ -> + {error, unacceptable_response} end; - decode_response({ok, {{_, 406, _}, _, _}}) -> %% the server respected our accept header and could not produce a response with any of %% the requested mime types: {error, unacceptable_response}; - decode_response({ok, {{_, Code, Status}, _, _}}) -> {error, {bad_response, {Code, Status}}}; - decode_response({error, _} = Error) -> Error. - decode_credentials(Plist) -> KeyPath = ['GetSessionTokenResponse', 'GetSessionTokenResult', 'Credentials'], case lists:foldl(fun (E, A) when is_list(A) -> erliam_util:getkey(E, A); (_, _) -> undefined - end, Plist, KeyPath) of - CredentialPlist when is_list(CredentialPlist) -> - awsv4:credentials_from_plist(convert_credential_plist(CredentialPlist)); - _ -> - {error, {bad_result, Plist}} + end, + Plist, + KeyPath) + of + CredentialPlist when is_list(CredentialPlist) -> + awsv4:credentials_from_plist(convert_credential_plist(CredentialPlist)); + _ -> + {error, {bad_result, Plist}} end. - convert_credential_plist(Plist) -> KeyMap = [{'AccessKeyId', access_key_id}, {'SecretAccessKey', secret_access_key}, {'Expiration', expiration}, {'SessionToken', token}], - [{erliam_util:getkey(K, KeyMap), binary_to_list(V)} - || {K, V} <- Plist]. + [{erliam_util:getkey(K, KeyMap), binary_to_list(V)} || {K, V} <- Plist]. diff --git a/src/erliam_sup.erl b/src/erliam_sup.erl index 87a6258..5833a76 100644 --- a/src/erliam_sup.erl +++ b/src/erliam_sup.erl @@ -3,26 +3,21 @@ -behaviour(supervisor). -export([start_link/1]). - -export([init/1]). -define(SERVER, ?MODULE). - %%%% API start_link(Opts) -> supervisor:start_link({local, ?SERVER}, ?MODULE, Opts). - %%%% SUPERVISOR CALLBACKS init(_) -> configure_httpc_profile(), - SupFlags = #{strategy => one_for_one, - intensity => 10, - period => 10}, + SupFlags = #{strategy => one_for_one, intensity => 10, period => 10}, CredSrv = #{id => erliam_srv, type => worker, @@ -31,7 +26,6 @@ init(_) -> {ok, {SupFlags, [CredSrv]}}. - %%%% INTERNAL FUNCTIONS configure_httpc_profile() -> diff --git a/src/erliam_util.erl b/src/erliam_util.erl index 3be5194..b7bff46 100644 --- a/src/erliam_util.erl +++ b/src/erliam_util.erl @@ -6,10 +6,7 @@ -module(erliam_util). --export([getkey/2, - find_header/2, - mime_type/1]). - +-export([getkey/2, find_header/2, mime_type/1]). %%%% API @@ -18,52 +15,49 @@ -spec mime_type(list()) -> string(). mime_type(Headers) -> case find_header("content-type", Headers) of - undefined -> - "text/plain"; - MimeType -> - [BaseType|_] = string:tokens(MimeType, ";"), - string:strip(BaseType) + undefined -> + "text/plain"; + MimeType -> + [BaseType | _] = string:tokens(MimeType, ";"), + string:strip(BaseType) end. - %% Return the named HTTP response header from the given proplist of headers %% (case-insensitive). --spec find_header(string(), list({string(), string()})) -> undefined | string(). +-spec find_header(string(), [{string(), string()}]) -> undefined | string(). find_header(Name, Headers) -> getkey(string:to_lower(Name), - [{string:to_lower(HeaderName), HeaderValue} - || {HeaderName, HeaderValue} <- Headers]). - + [{string:to_lower(HeaderName), HeaderValue} || {HeaderName, HeaderValue} <- Headers]). getkey(Key, Plist) -> case lists:keyfind(Key, 1, Plist) of - false -> - undefined; - {Key, Value} -> - Value + false -> + undefined; + {Key, Value} -> + Value end. - %%%% TESTS -ifdef(TEST). + -include_lib("eunit/include/eunit.hrl"). mime_type_test() -> - Headers = [{"Content-Type","text/plain; charset=utf-8"}, - {"Content-Length","15"}, - {"Date","Fri, 17 Oct 2014 21:41:13 GMT"}], - ?assertEqual("text/plain", mime_type(Headers)), + ?assertEqual("text/plain", mime_type(headers())), ?assertEqual("text/plain", mime_type([])), ?assertEqual("text/html", mime_type([{"content-type", "text/html; foo=bar"}])), ok. find_header_test() -> - Headers = [{"Content-Type","text/plain; charset=utf-8"}, - {"Content-Length","15"}, - {"Date","Fri, 17 Oct 2014 21:41:13 GMT"}], + Headers = headers(), ?assertEqual("15", find_header("content-length", Headers)), ?assertEqual("text/plain; charset=utf-8", find_header("Content-Type", Headers)), ?assertEqual(undefined, find_header("X-YZZY", Headers)), ok. +headers() -> + [{"Content-Type", "text/plain; charset=utf-8"}, + {"Content-Length", "15"}, + {"Date", "Fri, 17 Oct 2014 21:41:13 GMT"}]. + -endif. diff --git a/src/erliam_xml.erl b/src/erliam_xml.erl index 87b9acd..ad798d1 100644 --- a/src/erliam_xml.erl +++ b/src/erliam_xml.erl @@ -12,48 +12,41 @@ -include_lib("xmerl/include/xmerl.hrl"). - %%%% API parse(Xml) -> {E, _} = xmerl_scan:string(unicode:characters_to_list(Xml, utf8)), parse_element(E). - %%%% INTERNAL FUNCTIONS parse_element(#xmlElement{name = Name, content = Content}) -> - {Name, case Content of - [#xmlElement{} | _] -> - [{ChildName, parse_element(ChildContent)} - || #xmlElement{name = ChildName, - content = ChildContent} <- Content]; - _ -> - parse_element(Content) - end}; - + {Name, + case Content of + [#xmlElement{} | _] -> + [{ChildName, parse_element(ChildContent)} + || #xmlElement{name = ChildName, content = ChildContent} <- Content]; + _ -> + parse_element(Content) + end}; parse_element([#xmlText{} | _] = L) -> case lists:all(fun (E) -> - is_record(E, 'xmlText') - end, L) of - true -> - unescape_xml_text(xmerl_xs:value_of(L)); - - %% if a list of elements is not solely text, discard the text elements and parse - %% the non-text elements: - false -> - parse_element([X - || X <- L, - not is_record(X, 'xmlText')]) + is_record(E, xmlText) + end, + L) + of + true -> + unescape_xml_text(xmerl_xs:value_of(L)); + false -> + %% if a list of elements is not solely text, discard the text elements and parse + %% the non-text elements: + parse_element([X || X <- L, not is_record(X, xmlText)]) end; - parse_element(L) when is_list(L) -> [parse_element(X) || X <- L]; - parse_element(X) -> X. - unescape_xml_text(X) -> unescape_xml_text(unicode:characters_to_binary(X, utf8), [{<<"<">>, <<"<">>}, @@ -64,28 +57,23 @@ unescape_xml_text(X) -> unescape_xml_text(X, []) -> X; -unescape_xml_text(X, [{C, R}| T]) -> +unescape_xml_text(X, [{C, R} | T]) -> unescape_xml_text(binary:replace(X, C, R, [global]), T). - %%%% TESTS -ifdef(TEST). + -include_lib("eunit/include/eunit.hrl"). basic_decode_test() -> - Data = <<" - - - ACCESS_KEY_ID - SECRET_ACCESS_KEY - SESSION_TOKEN - EXPIRATION - - - - REQUEST_ID - - \n">>, + Data = <<"\n \n \n " + " ACCESS_KEY_ID\n SECR" + "ET_ACCESS_KEY\n SESSION_TOKEN\n EXPIRATION\n \n \n \n REQUEST_ID\n \n \n">>, ?assertEqual({'GetSessionTokenResponse', [{'GetSessionTokenResult', [{'Credentials', @@ -93,8 +81,7 @@ basic_decode_test() -> {'SecretAccessKey', <<"SECRET_ACCESS_KEY">>}, {'SessionToken', <<"SESSION_TOKEN">>}, {'Expiration', <<"EXPIRATION">>}]}]}, - {'ResponseMetadata', - [{'RequestId', <<"REQUEST_ID">>}]}]}, + {'ResponseMetadata', [{'RequestId', <<"REQUEST_ID">>}]}]}, parse(Data)). -endif. diff --git a/src/imds.erl b/src/imds.erl index 6d716ef..08690f1 100644 --- a/src/imds.erl +++ b/src/imds.erl @@ -21,7 +21,6 @@ -define(IMDS_TIMEOUT, 30000). -define(IMDS_RETRIES, 3). - %%%% API -spec role_name() -> {error, term()} | {ok, string()}. @@ -40,97 +39,94 @@ instance_id() -> public_hostname() -> imds_text("public-hostname"). - %% Obtain a session token from the instance metadata server, returning an %% awsv4:credentials(). -spec get_session_token() -> {error, term()} | awsv4:credentials(). get_session_token() -> case role_name() of - {ok, RoleName} -> - Result = imds_tokens(["iam/security-credentials/", http_uri:encode(RoleName)]), - awsv4:credentials_from_plist(Result); - Error -> - Error + {ok, RoleName} -> + Result = imds_tokens(["iam/security-credentials/", http_uri:encode(RoleName)]), + awsv4:credentials_from_plist(Result); + Error -> + Error end. - %% Make a GET request to the given URL, expecting (accepting) the given mime types, and %% with the given request timeout in milliseconds. --spec imds_response(string(), list(string()), pos_integer()) -> - {ok, term()} | {error, term()}. +-spec imds_response(string(), [string()], pos_integer()) -> {ok, term()} | + {error, term()}. imds_response(Url, MimeTypes, Timeout) -> RequestHeaders = [{"Accept", string:join(MimeTypes, ", ")}], - case httpc:request(get, {Url, RequestHeaders}, - [{timeout, Timeout}], [{body_format, binary}], - erliam:httpc_profile()) of - {ok, {{_, 200, _}, Headers, Body}} -> - case lists:member(erliam_util:mime_type(Headers), MimeTypes) of - true -> - {ok, Body}; - false -> - %% the server ignored our accept header: - {error, unacceptable_response} - end; - {ok, {{_, 406, _}, _, _}} -> - %% the server respected our accept header and could not produce a response - %% with any of the requested mime types: - {error, unacceptable_response}; - {ok, {{_, Code, Status}, _, _}} -> - {error, {bad_response, {Code, Status}}}; - {error, Reason} -> - {error, Reason} + case httpc:request(get, + {Url, RequestHeaders}, + [{timeout, Timeout}], + [{body_format, binary}], + erliam:httpc_profile()) + of + {ok, {{_, 200, _}, Headers, Body}} -> + case lists:member(erliam_util:mime_type(Headers), MimeTypes) of + true -> + {ok, Body}; + false -> + %% the server ignored our accept header: + {error, unacceptable_response} + end; + {ok, {{_, 406, _}, _, _}} -> + %% the server respected our accept header and could not produce a response + %% with any of the requested mime types: + {error, unacceptable_response}; + {ok, {{_, Code, Status}, _, _}} -> + {error, {bad_response, {Code, Status}}}; + {error, Reason} -> + {error, Reason} end. - %% As in imds_response/3, but retrying on failure. --spec imds_response(string(), list(string()), pos_integer(), pos_integer()) -> - {ok, term()} | {error, term()}. +-spec imds_response(string(), [string()], pos_integer(), pos_integer()) -> {ok, term()} | + {error, term()}. imds_response(Url, MimeTypes, Timeout, Retries) -> - call_with_retry(?MODULE, imds_response, [Url, MimeTypes, Timeout], + call_with_retry(?MODULE, + imds_response, + [Url, MimeTypes, Timeout], "Could not obtain IMDS response: ~p~n", Retries). - %%%% INTERNAL FUNCTIONS %% Call the given arity-1 Transform function with the result of a successful call to %% imds_response/4, or return the error which resulted from that call. --spec imds_transform_response(string(), list(string()), function()) -> - {error, term()} | term(). +-spec imds_transform_response(string(), [string()], function()) -> {error, term()} | + term(). imds_transform_response(Url, MimeTypes, Transform) -> case imds_response(Url, MimeTypes, ?IMDS_TIMEOUT, ?IMDS_RETRIES) of - {ok, Result} -> - Transform(Result); - Error -> - Error + {ok, Result} -> + Transform(Result); + Error -> + Error end. - %% Fetch the given Url and return the response as text (a string) if successful. --spec imds_text_response(string()) -> - {ok, string()} | {error, term()}. +-spec imds_text_response(string()) -> {ok, string()} | {error, term()}. imds_text_response(Url) -> - imds_transform_response(Url, ["text/plain"], + imds_transform_response(Url, + ["text/plain"], %% fixme; assumes utf-8 encoding. fun (Result) -> case unicode:characters_to_list(Result) of - {error, _, _} -> - {error, invalid_unicode}; - {incomplete, _, _} -> - {error, invalid_unicode}; - String -> - {ok, String} + {error, _, _} -> + {error, invalid_unicode}; + {incomplete, _, _} -> + {error, invalid_unicode}; + String -> + {ok, String} end end). %% Fetch the given Url and return the response as a proplist of tokens. --spec imds_token_response(string()) -> - list({atom(), string()}) | {error, term()}. +-spec imds_token_response(string()) -> [{atom(), string()}] | {error, term()}. imds_token_response(Url) -> MimeTypes = ["text/plain", "application/json"], - imds_transform_response(Url, MimeTypes, - fun metadata_response_to_token_proplist/1). - + imds_transform_response(Url, MimeTypes, fun metadata_response_to_token_proplist/1). %% Obtain relevant values from the JSON response body, returning a proplist with atom keys %% and the appropriate values. @@ -140,79 +136,74 @@ metadata_response_to_token_proplist(Body) -> {<<"SecretAccessKey">>, secret_access_key}, {<<"Token">>, token}], case get_code(Body) of - {success, Plist} -> - lists:foldl( - fun({Element, Value}, Acc) -> - case erliam_util:getkey(Element, Targets) of - undefined -> - Acc; - AtomName -> - [{AtomName, binary_to_list(Value)} | Acc] - end - end, [], Plist); - Error -> - Error + {success, Plist} -> + lists:foldl(fun ({Element, Value}, Acc) -> + case erliam_util:getkey(Element, Targets) of + undefined -> + Acc; + AtomName -> + [{AtomName, binary_to_list(Value)} | Acc] + end + end, + [], + Plist); + Error -> + Error end. get_code(Body) -> case jiffy:decode(Body) of - {Plist} -> - case erliam_util:getkey(<<"Code">>, Plist) of - <<"Success">> -> - {success, Plist}; - _ -> - {error, failed_token_response} - end; - _ -> - {error, invalid_token_json} + {Plist} -> + case erliam_util:getkey(<<"Code">>, Plist) of + <<"Success">> -> + {success, Plist}; + _ -> + {error, failed_token_response} + end; + _ -> + {error, invalid_token_json} end. - %% Call the given M:F with Args, emitting an error with the given ErrorFormat (with the %% error as the single format argument) and retrying otherwise. Does not catch exits. --spec call_with_retry(atom(), atom(), list(), string(), integer()) -> - {ok, term()} | {error, term()}. +-spec call_with_retry(atom(), atom(), list(), string(), integer()) -> {ok, term()} | + {error, term()}. call_with_retry(Module, Fun, Args, ErrorFormat, Retries) when Retries > 0 -> case apply(Module, Fun, Args) of - {ok, Result} -> - {ok, Result}; - Error -> - error_logger:error_msg(ErrorFormat, [Error]), - call_with_retry(Module, Fun, Args, ErrorFormat, Retries - 1) + {ok, Result} -> + {ok, Result}; + Error -> + error_logger:error_msg(ErrorFormat, [Error]), + call_with_retry(Module, Fun, Args, ErrorFormat, Retries - 1) end; call_with_retry(_, _, _, _, _) -> {error, retries_exceeded}. - imds_url(Suffix) -> lists:flatten(["http://", ?IMDS_HOST, "/", ?IMDS_VERSION, "/meta-data/", Suffix]). - imds_text(Suffix) -> imds_text_response(imds_url(Suffix)). - imds_tokens(Suffix) -> imds_token_response(imds_url(Suffix)). - %%%% UNIT TESTS -ifdef(TEST). + -include_lib("eunit/include/eunit.hrl"). metadata_response_to_proplist_test() -> - Body = << - "{\"Code\":\"Success\",\"LastUpdated\":\"2014-10-17T15:17:07-07:00\"," - "\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"XYZZY\",\"SecretAccessKey\":" - "\"FLOOBLE\",\"Token\":\"BAZZLE\",\"Expiration\":\"2014-10-18T09:00:30Z\"}" - >>, + Body = <<"{\"Code\":\"Success\",\"LastUpdated\":\"2014-10-17T15:17:07-07:00\",\"" + "Type\":\"AWS-HMAC\",\"AccessKeyId\":\"XYZZY\",\"SecretAccessKey\":\"" + "FLOOBLE\",\"Token\":\"BAZZLE\",\"Expiration\":\"2014-10-18T09:00:30Z\"" + "}">>, Result = metadata_response_to_token_proplist(Body), Expected = [{expiration, "2014-10-18T09:00:30Z"}, {access_key_id, "XYZZY"}, {secret_access_key, "FLOOBLE"}, {token, "BAZZLE"}], - [?assertEqual(erliam_util:getkey(Key, Expected), - erliam_util:getkey(Key, Result)) + [?assertEqual(erliam_util:getkey(Key, Expected), erliam_util:getkey(Key, Result)) || Key <- [expiration, access_key_id, secret_access_key, token]], ok.