Skip to content

Commit

Permalink
Support custom headers in Req.Utils.aws_sigv4_url/1
Browse files Browse the repository at this point in the history
  • Loading branch information
benjreinhart authored Jan 2, 2025
1 parent 67c5b9d commit dbf3602
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 14 deletions.
57 changes: 43 additions & 14 deletions lib/req/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ defmodule Req.Utils do
{method, options} = Keyword.pop!(options, :method)
{url, options} = Keyword.pop!(options, :url)
{expires, options} = Keyword.pop(options, :expires, 86400)
{headers, options} = Keyword.pop(options, :headers, [])
[] = options

datetime = DateTime.truncate(datetime, :second)
Expand All @@ -147,22 +148,25 @@ defmodule Req.Utils do
url = normalize_url(url)
service = to_string(service)

canonical_query_string =
URI.encode_query([
{"X-Amz-Algorithm", "AWS4-HMAC-SHA256"},
{"X-Amz-Credential", "#{access_key_id}/#{date_string}/#{region}/#{service}/aws4_request"},
{"X-Amz-Date", datetime_string},
{"X-Amz-Expires", expires},
{"X-Amz-SignedHeaders", "host"}
])
canonical_headers =
headers
|> canonical_host_header(url)
|> format_canonical_headers()

canonical_headers = canonical_host_header([], url)
signed_headers = Enum.map_join(canonical_headers, ";", &elem(&1, 0))

signed_headers =
Enum.map_intersperse(
Enum.sort(canonical_headers),
";",
&String.downcase(elem(&1, 0), :ascii)
canonical_query_string =
URI.encode_query(
[
{"X-Amz-Algorithm", "AWS4-HMAC-SHA256"},
{"X-Amz-Credential",
"#{access_key_id}/#{date_string}/#{region}/#{service}/aws4_request"},
{"X-Amz-Date", datetime_string},
{"X-Amz-Expires", expires},
{"X-Amz-SignedHeaders", signed_headers}
],
# Ensure spaces are encoded as %20 not +
:rfc3986
)

path = URI.encode(url.path || "/", &(&1 == ?/ or URI.char_unreserved?(&1)))
Expand Down Expand Up @@ -228,6 +232,31 @@ defmodule Req.Utils do
[{"host", host_value} | headers]
end

# Headers must be sorted alphabetically by name
# See https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
defp format_canonical_headers(headers) do
headers
|> Enum.map(&format_canonical_header/1)
|> Enum.sort(fn {name_1, _}, {name_2, _} -> name_1 < name_2 end)
end

# Header names must be lower case
# Header values must be trimmed
# See https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
defp format_canonical_header({name, value}) do
name =
name
|> to_string()
|> String.downcase(:ascii)

value =
value
|> to_string()
|> String.trim()

{name, value}
end

def aws_sigv4(
string_to_sign,
date_string,
Expand Down
28 changes: 28 additions & 0 deletions test/req/utils_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,34 @@ defmodule Req.UtilsTest do

assert url1 == url2
end

test "custom headers" do
options = [
access_key_id: "dummy-access-key-id",
secret_access_key: "dummy-secret-access-key",
region: "dummy-region",
service: "s3",
datetime: ~U[2024-01-01 09:00:00Z],
method: :put,
url: "https://s3/foo/hello_world.txt",
headers: [{"content-length", 11}]
]

url1 = to_string(Req.Utils.aws_sigv4_url(options))

url2 =
"""
https://s3/foo/hello_world.txt?\
X-Amz-Algorithm=AWS4-HMAC-SHA256\
&X-Amz-Credential=dummy-access-key-id%2F20240101%2Fdummy-region%2Fs3%2Faws4_request\
&X-Amz-Date=20240101T090000Z\
&X-Amz-Expires=86400\
&X-Amz-SignedHeaders=content-length%3Bhost\
&X-Amz-Signature=dbb4ae08836db5089a924f2eb52eb52dbc1c372a384a6a99ceb469b14b83e995\
"""

assert url1 == url2
end
end

describe "encode_form_multipart" do
Expand Down

0 comments on commit dbf3602

Please sign in to comment.