-
-
Save jeregrine/0d842c16de4ac4204c3d4b7bc62c1467 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
deps: | |
{:aws_signature, "~> 0.3.0"}, | |
{:aws_credentials, "~> 0.2.0"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Req.S3 do | |
def attach(request) do | |
request | |
|> Req.Request.register_options([:aws_endpoint]) | |
|> Req.Request.append_request_steps(sign_s3: &s3_sign/1) | |
end | |
defp s3_sign(request) do | |
if request.url.scheme == "s3" do | |
credentials = | |
case :aws_credentials.get_credentials() do | |
:undefined -> | |
raise ~s|aws_credentials could not find configured AWS Credentials. See https://hexdocs.pm/aws_credentials/readme.html for more. | | |
credentials -> | |
credentials | |
end | |
aws_endpoint = Map.get(request.options, :aws_endpoint, "s3.amazonaws.com") | |
host = "#{request.url.host}.#{aws_endpoint}" | |
url = %{request.url | scheme: "https", host: host, authority: host, port: 443} | |
now = NaiveDateTime.utc_now() |> NaiveDateTime.to_erl() | |
headers = | |
:aws_signature.sign_v4( | |
credentials.access_key_id, | |
credentials.secret_access_key, | |
credentials.region, | |
"s3", | |
now, | |
Atom.to_string(request.method), | |
URI.to_string(url), | |
Map.put(request.headers, "Host", host) |> headers(), | |
request.body || "", | |
uri_encode_path: false, | |
session_token: Map.get(credentials, :token, nil) | |
) | |
request | |
|> Map.replace!(:url, url) | |
|> Req.Request.put_headers(headers) | |
|> Req.Request.append_response_steps(req_s3_decode_body: &decode_body/1) | |
else | |
request | |
end | |
end | |
defp headers(headers) do | |
headers | |
|> Enum.map(fn | |
{k, v} when is_list(v) -> {k, Enum.join(v, ",")} | |
{k, v} -> {k, v} | |
end) | |
end | |
defp decode_body({request, response}) do | |
if request.url.path in [nil, "/"] && xml?(response.headers, response.body) do | |
body = response.body |> Req.S3.XML.decode!() | |
{request, %{response | body: body}} | |
else | |
{request, response} | |
end | |
end | |
defp xml?(headers, body) do | |
guess_xml? = String.starts_with?(body, "<?xml") | |
case Map.get(headers, "content-type") do | |
content_type when content_type in ["text/xml", "application/xml"] -> true | |
# Apparently some requests return XML without content-type | |
_ when guess_xml? -> true | |
_otherwise -> false | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Req.S3.XML do | |
# Adapted from https://github.com/livebook-dev/livebook/blob/main/lib/livebook/file_system/s3/xml.ex | |
# Adapted from https://github.com/aws-beam/aws-elixir/blob/v0.8.0/lib/aws/xml.ex | |
import Record | |
@text "__text" | |
defrecord(:xmlElement, extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")) | |
defrecord(:xmlText, extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")) | |
@doc """ | |
Encodes a map into XML iodata. | |
Raises in case of errors. | |
""" | |
@spec encode_to_iodata!(map()) :: iodata() | |
def encode_to_iodata!(map) do | |
map | |
|> Map.to_list() | |
|> Enum.map(&encode_xml_key_value/1) | |
|> :erlang.iolist_to_binary() | |
end | |
@doc """ | |
Decodes a XML into a map. | |
Raises in case of errors. | |
""" | |
@spec decode!(iodata()) :: map() | |
def decode!(xml) do | |
xml_str = :unicode.characters_to_list(xml) | |
opts = [{:hook_fun, &hook_fun/2}] | |
{element, []} = :xmerl_scan.string(xml_str, opts) | |
element | |
end | |
defp encode_xml_key_value({k, v}) when is_binary(k) and is_binary(v) do | |
["<", k, ">", escape_xml_string(v), "</", k, ">"] | |
end | |
defp encode_xml_key_value({k, values}) when is_binary(k) and is_list(values) do | |
for v <- values do | |
encode_xml_key_value({k, v}) | |
end | |
end | |
defp encode_xml_key_value({k, v}) when is_binary(k) and is_integer(v) do | |
["<", k, ">", Integer.to_charlist(v), "</", k, ">"] | |
end | |
defp encode_xml_key_value({k, v}) when is_binary(k) and is_float(v) do | |
["<", k, ">", Float.to_charlist(v), "</", k, ">"] | |
end | |
defp encode_xml_key_value({k, v}) when is_binary(k) and is_map(v) do | |
inner = v |> Map.to_list() |> Enum.map(&encode_xml_key_value/1) | |
["<", k, ">", inner, "</", k, ">"] | |
end | |
# Callback hook_fun for xmerl parser | |
defp hook_fun(element, global_state) when Record.is_record(element, :xmlElement) do | |
tag = xmlElement(element, :name) | |
content = xmlElement(element, :content) | |
value = | |
case List.foldr(content, :none, &content_to_map/2) do | |
v = %{@text => text} -> | |
case String.trim(text) do | |
"" -> Map.delete(v, @text) | |
trimmed -> Map.put(v, @text, trimmed) | |
end | |
v -> | |
v | |
end | |
{%{Atom.to_string(tag) => value}, global_state} | |
end | |
defp hook_fun(text, global_state) when Record.is_record(text, :xmlText) do | |
text = xmlText(text, :value) | |
{:unicode.characters_to_binary(text), global_state} | |
end | |
# Convert the content of an Xml node into a map. | |
# When there is more than one element with the same tag name, their | |
# values get merged into a list. | |
# If the content is only text then that is what gets returned. | |
# If the content is a mix between text and child elements, then the | |
# elements are processed as described above and all the text parts | |
# are merged under the `__text' key. | |
defp content_to_map(x, :none) do | |
x | |
end | |
defp content_to_map(x, acc) when is_map(x) and is_map(acc) do | |
[{tag, value}] = Map.to_list(x) | |
case Map.has_key?(acc, tag) do | |
true -> | |
update_fun = fn | |
l when is_list(l) -> [value | l] | |
v -> [value, v] | |
end | |
Map.update!(acc, tag, update_fun) | |
false -> | |
Map.put(acc, tag, value) | |
end | |
end | |
defp content_to_map(x, %{@text => text} = acc) when is_binary(x) and is_map(acc) do | |
%{acc | @text => <<x::binary, text::binary>>} | |
end | |
defp content_to_map(x, acc) when is_binary(x) and is_map(acc) do | |
Map.put(acc, @text, x) | |
end | |
defp content_to_map(x, acc) when is_binary(x) and is_binary(acc) do | |
<<x::binary, acc::binary>> | |
end | |
defp content_to_map(x, acc) when is_map(x) and is_binary(acc) do | |
Map.put(x, @text, acc) | |
end | |
# See https://github.com/ex-aws/ex_aws_s3/blob/6973f72b0b78d928c86252767e7dcebab5be0ba8/lib/ex_aws/s3.ex#L1268 | |
defp escape_xml_string(value) do | |
String.replace(value, ["'", "\"", "&", "<", ">", "\r", "\n"], fn | |
"'" -> "'" | |
"\"" -> """ | |
"&" -> "&" | |
"<" -> "<" | |
">" -> ">" | |
"\r" -> " " | |
"\n" -> " " | |
end) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment