Skip to content

Instantly share code, notes, and snippets.

@ceolinrenato
Created August 18, 2021 01:37
Show Gist options
  • Save ceolinrenato/cc7f036ef7867c4ccd08ddfd932d1520 to your computer and use it in GitHub Desktop.
Save ceolinrenato/cc7f036ef7867c4ccd08ddfd932d1520 to your computer and use it in GitHub Desktop.
defmodule Core.Adapters.RemoteStorage.AWS do
@moduledoc """
Adapter for AWS S3 implementing the RemoteStorage interface
"""
@behaviour Core.Ports.RemoteStorage
@impl true
def upload_file(file, reference) do
md5 =
:md5
|> :crypto.hash(file)
|> Base.encode64()
%{remote_storage_bucket: remote_storage_bucket} = aws_config()
with {:ok, _, %{status_code: 200}} <-
AWS.S3.put_object(aws_client(), remote_storage_bucket, reference, %{
"Body" => file,
"ContentMD5" => md5,
"ContentType" => get_mime_type(reference)
}) do
:ok
end
end
@impl true
def download_file(reference) do
%{remote_storage_bucket: remote_storage_bucket} = aws_config()
with {:ok, %{"Body" => file}, _} <-
AWS.S3.get_object(aws_client(), remote_storage_bucket, reference) do
{:ok, file}
end
end
@impl true
def signed_uri(reference, ttl) do
%{
remote_storage_bucket: remote_storage_bucket,
access_key_id: access_key_id,
remote_storage_region: remote_storage_region
} = aws_config()
uri =
URI.parse(
"https://#{remote_storage_bucket}.s3.#{remote_storage_region}.#{aws_client().endpoint}/#{AWS.Util.encode_uri(reference, true)}"
)
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
long_date = NaiveDateTime.to_iso8601(now, :basic) <> "Z"
short_date = Date.to_iso8601(now, :basic)
query_params = [
{"X-Amz-Expires", Integer.to_string(ttl)},
{"X-Amz-Algorithm", "AWS4-HMAC-SHA256"},
{"X-Amz-Credential",
"#{access_key_id}/#{short_date}/#{remote_storage_region}/s3/aws4_request"},
{"X-Amz-SignedHeaders", "host"},
{"X-Amz-Date", long_date}
]
url = URI.to_string(%{uri | query: URI.encode_query(query_params)})
canonical_request = canonical_request("GET", url, [{"Host", uri.host}], nil)
hashed_canonical_request = AWS.Util.sha256_hexdigest(canonical_request)
credential_scope = credential_scope(short_date, remote_storage_region, "s3")
signing_key = signing_key(%{aws_client() | service: "s3"}, short_date)
string_to_sign = string_to_sign(long_date, credential_scope, hashed_canonical_request)
signature = AWS.Util.hmac_sha256_hexdigest(signing_key, string_to_sign)
URI.to_string(%{
uri
| query: URI.encode_query([{"X-Amz-Signature", signature} | query_params])
})
end
defp string_to_sign(long_date, credential_scope, hashed_canonical_request) do
Enum.join(
["AWS4-HMAC-SHA256", long_date, credential_scope, hashed_canonical_request],
"\n"
)
end
defp aws_client do
%{
access_key_id: access_key_id,
secret_access_key: secret_access_key,
remote_storage_region: remote_storage_region
} = aws_config()
AWS.Client.create(access_key_id, secret_access_key, remote_storage_region)
end
defp aws_config do
:core
|> Application.fetch_env!(:aws)
|> Map.new()
end
defp get_mime_type(path) do
[[_, _file_name, format]] = Regex.scan(~r</([^/]+)\.([a-z0-9]+)$>, path)
MIME.type(format)
end
defp signing_key(%AWS.Client{} = client, short_date) do
("AWS4" <> client.secret_access_key)
|> AWS.Util.hmac_sha256(short_date)
|> AWS.Util.hmac_sha256(client.region)
|> AWS.Util.hmac_sha256(client.service)
|> AWS.Util.hmac_sha256("aws4_request")
end
defp credential_scope(short_date, region, service) do
Enum.join([short_date, region, service, "aws4_request"], "/")
end
defp canonical_request(method, url, headers, nil) do
{canonical_url, canonical_query_string} = split_url(url)
canonical_headers = canonical_headers(headers)
signed_headers = signed_headers(headers)
Enum.join(
[
method,
canonical_url,
canonical_query_string,
canonical_headers,
signed_headers,
"UNSIGNED-PAYLOAD"
],
"\n"
)
end
defp canonical_request(method, url, headers, body) do
{canonical_url, canonical_query_string} = split_url(url)
canonical_headers = canonical_headers(headers)
signed_headers = signed_headers(headers)
payload_hash = AWS.Util.sha256_hexdigest(body)
Enum.join(
[
method,
canonical_url,
canonical_query_string,
canonical_headers,
signed_headers,
payload_hash
],
"\n"
)
end
defp split_url(url) do
url = URI.parse(url)
{AWS.Signature.uri_encode(url.path), normalize_query(url.query)}
end
defp canonical_headers(headers) do
headers
|> Enum.map(fn {name, value} ->
name =
name
|> String.downcase()
|> String.trim()
value = String.trim(value)
{name, value}
end)
|> Enum.sort(fn {a, _}, {b, _} -> a <= b end)
|> Enum.map(fn {name, value} -> [name, ":", value, "\n"] end)
|> Enum.join()
end
defp signed_headers(headers) do
headers
|> Enum.map(fn {name, _value} -> name |> String.downcase() |> String.trim() end)
|> Enum.sort()
|> Enum.join(";")
end
defp normalize_query(nil), do: ""
defp normalize_query(""), do: ""
defp normalize_query(query) do
query
|> String.split("&")
|> Enum.map(&String.split(&1, "="))
|> Enum.sort(fn [a, _], [b, _] -> a <= b end)
|> Enum.map_join("&", fn
[key, value] -> key <> "=" <> value
[key] -> key <> "="
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment