Skip to content

Instantly share code, notes, and snippets.

@sikanhe
Created March 24, 2016 21:01

Revisions

  1. sikanhe created this gist Mar 24, 2016.
    210 changes: 210 additions & 0 deletions aws_snppet.ex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,210 @@
    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