|
4 | 4 | aws_request_xml/5, aws_request_xml/6, aws_request_xml/7, aws_request_xml/8,
|
5 | 5 | aws_request2/7,
|
6 | 6 | aws_request_xml2/5, aws_request_xml2/7,
|
| 7 | + aws_request_xml4/6,aws_request_xml4/8, |
7 | 8 | aws_request_form/8,
|
8 | 9 | param_list/2, default_config/0, update_config/1, format_timestamp/1,
|
9 | 10 | http_headers_body/1,
|
@@ -43,6 +44,16 @@ aws_request_xml2(Method, Protocol, Host, Port, Path, Params, #aws_config{} = Con
|
43 | 44 | {error, Reason}
|
44 | 45 | end.
|
45 | 46 |
|
| 47 | +aws_request_xml4(Method, Host, Path, Params, Service, #aws_config{} = Config) -> |
| 48 | + aws_request_xml4(Method, undefined, Host, undefined, Path, Params, Service, Config). |
| 49 | +aws_request_xml4(Method, Protocol, Host, Port, Path, Params, Service, #aws_config{} = Config) -> |
| 50 | + case aws_request4(Method, Protocol, Host, Port, Path, Params, Service, Config) of |
| 51 | + {ok, Body} -> |
| 52 | + {ok, element(1, xmerl_scan:string(binary_to_list(Body)))}; |
| 53 | + {error, Reason} -> |
| 54 | + {error, Reason} |
| 55 | + end. |
| 56 | + |
46 | 57 | aws_request(Method, Host, Path, Params, #aws_config{} = Config) ->
|
47 | 58 | aws_request(Method, undefined, Host, undefined, Path, Params, Config).
|
48 | 59 | aws_request(Method, Host, Path, Params, AccessKeyID, SecretAccessKey) ->
|
@@ -89,6 +100,38 @@ aws_request2_no_update(Method, Protocol, Host, Port, Path, Params, #aws_config{}
|
89 | 100 |
|
90 | 101 | aws_request_form(Method, Protocol, Host, Port, Path, Query, [], Config).
|
91 | 102 |
|
| 103 | +aws_request4(Method, Protocol, Host, Port, Path, Params, Service, Config) -> |
| 104 | + case update_config(Config) of |
| 105 | + {ok, Config1} -> |
| 106 | + aws_request4_no_update(Method, Protocol, Host, Port, Path, Params, Service, Config1); |
| 107 | + {error, Reason} -> |
| 108 | + {error, Reason} |
| 109 | + end. |
| 110 | + |
| 111 | +aws_request4_no_update(Method, Protocol, Host, Port, Path, Params, Service, #aws_config{} = Config) -> |
| 112 | + QueryToSign = erlcloud_http:make_query_string(Params), |
| 113 | + |
| 114 | + Headers = [{"host", Host}], |
| 115 | + |
| 116 | + Region = |
| 117 | + case string:tokens(Host, ".") of |
| 118 | + [_, Value, _, _] -> |
| 119 | + Value; |
| 120 | + _ -> |
| 121 | + "us-east-1" |
| 122 | + end, |
| 123 | + |
| 124 | + SignedHeaders = case Method of |
| 125 | + get -> sign_v4(Method, Config, Headers, Params, "", Region, Service); |
| 126 | + post -> sign_v4(Method, Config, Headers, "", QueryToSign, Region, Service) |
| 127 | + end, |
| 128 | + |
| 129 | + aws_request_form(Method, Protocol, Host, Port, Path, QueryToSign, SignedHeaders, Config). |
| 130 | + |
| 131 | + |
| 132 | +-spec aws_request_form(Method :: atom(), Protocol :: undefined | string(), Host :: string(), |
| 133 | + Port :: undefined | integer() | string(), Path :: string(), Form :: string(), |
| 134 | + Headers :: list(), Config :: aws_config()) -> {ok, binary()} | {error, tuple()}. |
92 | 135 | aws_request_form(Method, Protocol, Host, Port, Path, Form, Headers, Config) ->
|
93 | 136 | UProtocol = case Protocol of
|
94 | 137 | undefined -> "https://";
|
@@ -276,13 +319,19 @@ request_to_return(#aws_request{response_type = error,
|
276 | 319 | %% TODO additional parameters - currently only supports what is needed for DynamoDB
|
277 | 320 | -spec sign_v4(aws_config(), headers(), binary(), string(), string()) -> headers().
|
278 | 321 | sign_v4(Config, Headers, Payload, Region, Service) ->
|
| 322 | + sign_v4(post, Config, Headers, [], Payload, Region, Service). |
| 323 | + |
| 324 | +-spec sign_v4(atom(), aws_config(), headers(), binary(), binary(), string(), string()) -> headers(). |
| 325 | +sign_v4(Method, Config, Headers, QueryParams, Payload, Region, Service) -> |
279 | 326 | Date = iso_8601_basic_time(),
|
280 | 327 | Headers1 = [{"x-amz-date", Date} | Headers],
|
281 | 328 | Headers2 = case Config#aws_config.security_token of
|
282 | 329 | undefined -> Headers1;
|
283 | 330 | Token -> [{"x-amz-security-token", Token} | Headers1]
|
284 | 331 | end,
|
285 |
| - {Request, SignedHeaders} = canonical_request("POST", "/", "", Headers2, Payload), |
| 332 | + CanonicalQueryString = canonical_query_string(QueryParams), |
| 333 | + MethodString = string:to_upper(atom_to_list(Method)), |
| 334 | + {Request, SignedHeaders} = canonical_request(MethodString, "/", CanonicalQueryString, Headers2, Payload), |
286 | 335 | CredentialScope = credential_scope(Date, Region, Service),
|
287 | 336 | ToSign = to_sign(Date, CredentialScope, Request),
|
288 | 337 | SigningKey = signing_key(Config, Date, Region, Service),
|
@@ -313,6 +362,18 @@ canonical_headers(Headers) ->
|
313 | 362 | Signed = string:join([Name || {Name, _} <- Sorted], ";"),
|
314 | 363 | {Canonical, Signed}.
|
315 | 364 |
|
| 365 | +%% @doc calculate canonical query string out of query params and according to v4 documentation |
| 366 | +canonical_query_string([]) -> |
| 367 | + ""; |
| 368 | +canonical_query_string(Params) -> |
| 369 | + Normalized = [{erlcloud_http:url_encode(Name), erlcloud_http:url_encode(erlcloud_http:value_to_string(Value))} || {Name, Value} <- Params], |
| 370 | + Sorted = lists:keysort(1, Normalized), |
| 371 | + string:join([case Value of |
| 372 | + [] -> [Key, "="]; |
| 373 | + _ -> [Key, "=", Value] |
| 374 | + end |
| 375 | + || {Key, Value} <- Sorted, Value =/= none, Value =/= undefined], "&"). |
| 376 | + |
316 | 377 | trimall(Value) ->
|
317 | 378 | %% TODO - remove excess internal whitespace in header values
|
318 | 379 | re:replace(Value, "(^\\s+)|(\\s+$)", "", [global]).
|
|
0 commit comments