Packages
aws
0.13.0
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
AWS clients for Elixir
Current section
Files
Jump to
Current section
Files
lib/aws/request.ex
defmodule AWS.Request do
@moduledoc false
alias AWS.Client
alias AWS.Signature
@valid_protocols ~w(query json rest-json rest-xml)
@doc """
Request an AWS Service using a POST request with a protocol.
"""
def request_post(%Client{} = client, %{} = metadata, action, input, options) do
client = prepare_client(client, metadata)
host = build_host(client, metadata)
url =
client
|> build_uri(host)
|> to_string()
headers = [
{"Host", host},
{"Content-Type", metadata.content_type}
]
headers =
if metadata.protocol == "json" do
[{"X-Amz-Target", "#{metadata.target_prefix}.#{action}"} | headers]
else
headers
end
input =
if metadata.protocol == "query" do
Map.merge(input, %{"Action" => action, "Version" => metadata.api_version})
else
input
end
payload = encode!(client, metadata.protocol, input)
headers = Signature.sign_v4(client, now(), "POST", url, headers, payload)
case AWS.Client.request(client, :post, url, payload, headers, options) do
{:ok, %{status_code: 200, body: body} = response} ->
body = if body != "", do: decode!(client, metadata.protocol, body)
{:ok, body, response}
{:ok, response} ->
{:error, {:unexpected_response, response}}
error = {:error, _reason} ->
error
end
end
def request_rest(
%Client{} = client,
%{} = metadata,
http_method,
path,
query,
headers,
input,
options,
success_status_code
) do
client = prepare_client(client, metadata)
input = input || %{}
host = build_host(client, metadata, headers)
url =
client
|> build_uri(host, path)
|> add_query(query, client)
|> to_string()
{send_body_as_binary?, options} = Keyword.pop(options, :send_body_as_binary?)
# It will assume the default as "application/octet-stream" if is sending body
# as binary and the input does not specify the `Content-type`.
default_content_type =
if send_body_as_binary? do
"application/octet-stream"
else
metadata.content_type
end
additional_headers = [{"Host", host}, {"Content-Type", default_content_type}]
headers = add_headers(additional_headers, headers)
payload =
cond do
send_body_as_binary? ->
Map.fetch!(input, "Body")
http_method in [:get, :head, :options] ->
""
true ->
encode!(client, metadata.protocol, input)
end
headers = Signature.sign_v4(client, now(), http_method, url, headers, payload)
{response_header_parameters, options} = Keyword.pop(options, :response_header_parameters)
case Client.request(client, http_method, url, payload, headers, options) do
{:ok, %{status_code: status_code, body: body} = response}
when (is_nil(success_status_code) and status_code in 200..299) or
status_code == success_status_code ->
body =
if body != "" do
{receive_body_as_binary?, _options} = Keyword.pop(options, :receive_body_as_binary?)
response_body =
if receive_body_as_binary? do
%{"Body" => body}
else
decode!(client, metadata.protocol, body)
end
case response_header_parameters do
[_ | _] ->
response_body =
if is_binary(response_body) do
%{"Body" => response_body}
else
response_body
end
merge_body_with_response_headers(
response_body,
response,
response_header_parameters
)
_ ->
response_body
end
end
{:ok, body, response}
{:ok, response} ->
{:error, {:unexpected_response, response}}
error = {:error, _reason} ->
error
end
end
defp prepare_client(client, metadata) do
client = %{client | service: metadata.signing_name}
if metadata.global? and client.region != "local" do
%{client | region: metadata.credential_scope}
else
client
end
end
defp build_host(%Client{} = client, %{} = metadata, headers \\ []) do
build_options = %{
region: client.region,
endpoint: client.endpoint,
service: metadata.signing_name,
global?: metadata.global?,
endpoint_prefix: metadata.endpoint_prefix,
account_id: :proplists.get_value("x-amz-account-id", headers, nil)
}
case build_options do
%{region: "local", endpoint: nil} ->
"localhost"
%{endpoint: endpoint} when is_binary(endpoint) ->
endpoint
%{endpoint: endpoint_fun} when is_function(endpoint_fun, 1) ->
endpoint_fun.(build_options)
%{global?: true, endpoint: endpoint} ->
endpoint = resolve_endpoint_sufix(endpoint)
build_final_endpoint(
[to_string(metadata[:host_prefix]) <> metadata.endpoint_prefix, endpoint],
build_options
)
%{endpoint: endpoint} ->
endpoint = resolve_endpoint_sufix(endpoint)
build_final_endpoint(
[
to_string(metadata[:host_prefix]) <> metadata.endpoint_prefix,
client.region,
endpoint
],
build_options
)
end
end
defp resolve_endpoint_sufix({:keep_prefixes, sufix}) when is_binary(sufix) do
sufix
end
defp resolve_endpoint_sufix(nil), do: Client.default_endpoint()
defp build_final_endpoint(parts, options) do
parts =
if options.endpoint_prefix == "s3-control" do
account_id = options.account_id || raise "missing account_id"
[account_id | parts]
else
parts
end
joined = Enum.join(parts, ".")
# TODO: handle more cases other than `AccountId`
if options[:account_id] do
joined |> String.replace("{AccountId}", options.account_id, global: true)
else
joined
end
end
defp build_uri(%Client{} = client, host, path \\ "/") do
URI.merge("#{client.proto}://#{host}:#{client.port}", path)
end
defp add_query(uri = %URI{}, [], _client) do
uri
end
defp add_query(uri = %URI{}, query, client) do
querystring = Client.encode!(client, query, :query)
if is_binary(uri.query) do
%{uri | query: uri.query <> "&" <> querystring}
else
%{uri | query: querystring}
end
end
defp encode!(%Client{} = client, protocol, payload)
when protocol in @valid_protocols and is_map(payload) do
encode_format =
case protocol do
"query" -> :query
"rest-xml" -> :xml
json_type when json_type in ~w(json rest-json) -> :json
end
Client.encode!(client, payload, encode_format)
end
defp decode!(%Client{} = client, protocol, payload) when protocol in @valid_protocols do
decode_format =
case protocol do
xml_type when xml_type in ~w(query rest-xml) -> :xml
json_type when json_type in ~w(json rest-json) -> :json
end
Client.decode!(client, payload, decode_format)
end
defp merge_body_with_response_headers(body, response, response_header_parameters) do
Enum.reduce(response_header_parameters, body, fn {header_name, key}, acc ->
case List.keyfind(response.headers, header_name, 0) do
nil -> acc
{_header_name, value} -> Map.put(acc, key, value)
end
end)
end
@doc """
Build request params (header or querystring) based on a list of key-value
pairs representing the `mapping` from the input name (key) to the param
name (value) and a map with the input `params`.
"""
def build_params(mapping, %{} = params) when is_list(mapping) do
Enum.reduce(mapping, {[], params}, &move_param_to_header/2)
end
defp move_param_to_header({param_name, header_name}, {headers, params}) do
case Map.get(params, param_name) do
nil ->
{headers, params}
param_value ->
headers = [{header_name, param_value} | headers]
params = Map.delete(params, param_name)
{headers, params}
end
end
@doc """
Include additions only if they do not already exist in the provided list.
"""
def add_headers([], headers) do
headers
end
def add_headers([{name, _} = header | additions], headers) do
case List.keyfind(headers, name, 0) do
nil -> add_headers(additions, [header | headers])
{_, _} -> add_headers(additions, headers)
end
end
defp now do
NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
end
end