Skip to content

Instantly share code, notes, and snippets.

@sikanhe
Created March 24, 2016 21:01
Show Gist options
  • Save sikanhe/cfc636ea2c8a25ed7c86 to your computer and use it in GitHub Desktop.
Save sikanhe/cfc636ea2c8a25ed7c86 to your computer and use it in GitHub Desktop.
defmodule AWS.S3 do
import AWS.Utils
@config Application.get_env(:vino, __MODULE__)
@allowed_file_types ~w(.jpeg .jpg .png)
def base_url(bucket) do
"https://#{bucket}.s3.amazonaws.com/"
end
@doc """
Upload an image using put request to s3
"""
def put_object(bucket, binary, key, opts \\ []) do
acl = opts[:acl] || "public-read"
file_size = byte_size(binary) |> Integer.to_string
url = Path.join base_url(bucket), key
opts_headers = opts[:headers] || []
headers = [
{"Content-Length", file_size},
{"x-amz-acl", acl},
{"x-amz-content-sha256", "UNSIGNED-PAYLOAD"}
] ++ opts_headers
headers = Vino.AWS.Auth.build_headers(:put, url, "s3", headers, nil, @config)
with :ok <- do_upload(url, binary, headers, [retries: 4]), do: {:ok, key}
end
def put_object!(bucket, binary, key, opts \\ []) do
case put_object(bucket, binary, key, opts) do
{:ok, key} -> key
{:error, error} -> raise error
end
end
@doc """
Upload a file with retries attempt
"""
def do_upload(url, binary, headers, [retries: count]) when is_integer(count) do
case HTTPoison.put(url, binary, headers, [recv_timeout: :infinity, timeout: :infinity]) do
{:error, error} ->
IO.inspect error
if count > 0 do
do_upload(url, binary, headers, [retries: count - 1])
else
{:error, error}
end
{:ok, %{status_code: 200}} ->
:ok
{:ok, %{status_code: ___, body: body}} ->
if count > 0 do
do_upload(url, binary, headers, [retries: count - 1])
else
{:error, body}
end
end
end
def delete_object(bucket, key) do
url = Path.join base_url(bucket), key
headers = [
{"x-amz-content-sha256", "UNSIGNED-PAYLOAD"}
]
headers = Vino.AWS.Auth.build_headers(:delete, url, "s3", headers, nil, @config)
do_delete(url, headers, [retries: 4])
end
def do_delete(url, headers, [retries: count]) do
case HTTPoison.delete!(url, headers, [recv_timeout: 20*1000, timeout: 10*1000]) do
%{status_code: 204} ->
:ok
%{status_code: ___, body: body} ->
cond do
count > 0 ->
do_delete(url, headers, [retries: count - 1])
true ->
{:error, body}
end
end
end
end
defmodule AWS.Auth do
@moduledoc """
This module contains all the function needed to build a header for
"Authorization" field with Amazon Signature Version 4
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
"""
use Timex
import AWS.Utils
def build_headers(http_method, url, service, headers, body, config) do
timestamp = Date.now
headers = [
{"host", URI.parse(url).host},
{"x-amz-date", amz_date(timestamp)}
] ++ headers
auth_header = auth_header(
http_method,
url,
headers,
body,
service,
timestamp,
config)
[{"Authorization", auth_header}] ++ headers
end
defp auth_header(http_method, url, headers, body, service, timestamp, config) do
signature = signature(http_method, url, headers, body, service, timestamp, config)
[
"AWS4-HMAC-SHA256 Credential=", credentials(service, timestamp, config), ",",
"SignedHeaders=", signed_headers(headers), ",",
"Signature=", signature
]
|> IO.iodata_to_binary
end
def signature(http_method, url, headers, body, service, timestamp, config) do
build_canonical_request(http_method, url, headers, body)
|> string_to_sign(service, timestamp, config)
|> sign_aws_s4(config[:secret_key], date(timestamp), config[:region], service)
end
def build_canonical_request(http_method, url, headers, body) do
uri = URI.parse(url)
http_method = http_method |> Atom.to_string |> String.upcase
query_params = uri.query |> canonical_query_params
headers = headers |> canonical_headers
header_string = headers
|> Enum.map(fn {k, v} -> "#{k}:#{v}" end)
|> Enum.join("\n")
signed_headers_list = headers
|> Keyword.keys
|> Enum.join(";")
payload = case body do
nil -> "UNSIGNED-PAYLOAD"
_ -> hash_sha256(body)
end
[
http_method, "\n",
uri_encode(uri.path), "\n",
query_params, "\n",
header_string, "\n",
"\n",
signed_headers_list, "\n",
payload
] |> IO.iodata_to_binary
end
defp string_to_sign(request, service, timestamp, config) do
request = hash_sha256(request)
"""
AWS4-HMAC-SHA256
#{amz_date(timestamp)}
#{scope(service, timestamp, config)}
#{request}
"""
|> String.rstrip
end
defp signed_headers(headers) do
headers
|> Enum.map(fn({k, _}) -> String.downcase(k) end)
|> Enum.sort(&(&1 < &2))
|> Enum.join(";")
end
defp canonical_query_params(nil), do: ""
defp canonical_query_params(params) do
params
|> URI.query_decoder
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end)
|> URI.encode_query
end
defp canonical_headers(headers) do
headers
|> Enum.map(fn
{k, v} when is_binary(v) -> {String.downcase(k), String.strip(v)}
{k, v} -> {String.downcase(k), v}
end)
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end)
end
defp credentials(service, timestamp, config) do
"#{config[:access_key]}/#{scope(service, timestamp, config)}"
end
defp scope(service, timestamp, config) do
"#{date(timestamp)}/#{config[:region]}/#{service}/aws4_request"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment