diff --git a/lib/ex_aws/auth.ex b/lib/ex_aws/auth.ex index 065e1563..b7d79469 100644 --- a/lib/ex_aws/auth.ex +++ b/lib/ex_aws/auth.ex @@ -157,7 +157,7 @@ defmodule ExAws.Auth do end defp signature(http_method, url, query, headers, body, service, datetime, config) do - path = url |> Url.get_path(service) |> Url.uri_encode() + path = url |> Url.get_path(service) request = build_canonical_request(http_method, path, query, headers, body) string_to_sign = string_to_sign(request, service, datetime, config) Signatures.generate_signature_v4(service, config, datetime, string_to_sign) diff --git a/lib/ex_aws/operation/s3.ex b/lib/ex_aws/operation/s3.ex index 3ec2d521..99417317 100644 --- a/lib/ex_aws/operation/s3.ex +++ b/lib/ex_aws/operation/s3.ex @@ -27,6 +27,7 @@ defmodule ExAws.Operation.S3 do url = operation |> add_resource_to_params() + |> encode_path_query_fragment_into_path() |> ExAws.Request.Url.build(config) hashed_payload = ExAws.Auth.Utils.hash_sha256(body) @@ -42,6 +43,11 @@ defmodule ExAws.Operation.S3 do |> operation.parser.() end + def encode_path_query_fragment_into_path(%ExAws.Operation.S3{path: path} = operation) do + new_path = ExAws.Request.Url.uri_encode(path) + %ExAws.Operation.S3{operation | path: new_path} + end + def stream!(%{stream_builder: fun}, config), do: fun.(config) defp put_content_length_header(headers, "", :get), do: headers diff --git a/lib/ex_aws/request/url.ex b/lib/ex_aws/request/url.ex index 39ba77d6..550d3649 100644 --- a/lib/ex_aws/request/url.ex +++ b/lib/ex_aws/request/url.ex @@ -14,7 +14,7 @@ defmodule ExAws.Request.Url do |> convert_port_to_integer |> (&struct(URI, &1)).() |> URI.to_string() - |> String.trim_trailing("?") + |> String.replace_suffix("?", "") end defp query(operation) do @@ -52,7 +52,6 @@ defmodule ExAws.Request.Url do url |> get_path(service) |> String.replace_prefix("/", "") - |> uri_encode() query = case String.split(url, "?", parts: 2) do diff --git a/test/ex_aws/auth_test.exs b/test/ex_aws/auth_test.exs index 77ace9ba..87a933c7 100644 --- a/test/ex_aws/auth_test.exs +++ b/test/ex_aws/auth_test.exs @@ -81,7 +81,7 @@ defmodule ExAws.AuthTest do "&X-Amz-Date=20130524T000000Z" <> "&X-Amz-Expires=86400" <> "&X-Amz-SignedHeaders=host" <> - "&X-Amz-Signature=d1892eeaf3110a6c1a805d8ad7a0c825a72a4255c7f48908922be55a7c4ae753" + "&X-Amz-Signature=a515d441eea0607e063a287d32c9070d5d473bc5e47f4e8d61701e0a1aec84bc" assert {:ok, expected} == actual end diff --git a/test/ex_aws/operation/s3_test.exs b/test/ex_aws/operation/s3_test.exs index 0e7f7556..d42fc2f3 100644 --- a/test/ex_aws/operation/s3_test.exs +++ b/test/ex_aws/operation/s3_test.exs @@ -3,6 +3,8 @@ defmodule ExAws.Operation.S3Test do alias Elixir.ExAws.Operation.ExAws.Operation.S3 + import Mox + def s3_operation(bucket \\ "my-bucket-1") do %ExAws.Operation.S3{ body: "", @@ -80,4 +82,109 @@ defmodule ExAws.Operation.S3Test do {processed_operation, _processed_config} = S3.add_bucket_to_path(operation, config) assert processed_operation.path == "/folder/" end + + test "S3 object encoding with query parameter seperator (?)" do + config = %{ + http_client: ExAws.Request.HttpMock, + json_codec: JSX, + access_key_id: "AKIAIOSFODNN7EXAMPLE", + secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + region: "us-east-1", + retries: [ + max_attempts: 5, + base_backoff_in_ms: 1, + max_backoff_in_ms: 20 + ], + normalize_path: false, + scheme: "https://", + host: "s3.amazonaws.com", + virtual_host: true, + port: 443 + } + + headers = %{ + "x-amz-bucket-region" => "us-east-1", + "x-amz-content-sha256" => ExAws.Auth.Utils.hash_sha256(""), + "content-length" => byte_size("") + } + + expect( + ExAws.Request.HttpMock, + :request, + fn _method, url, _body, _headers, _opts -> + assert url == "https://examplebucket.s3.amazonaws.com/test%20hello%20%233.txt%3Facl%3D21" + {:ok, %{status_code: 200}} + end + ) + + operation = %ExAws.Operation.S3{ + s3_operation() + | path: "test hello #3.txt?acl=21", + headers: headers, + bucket: "examplebucket" + } + + assert {:ok, %{status_code: 200}} == S3.perform(operation, config) + end + + test "S3 object encoding with query parameter seperator (?) and version_id" do + config = %{ + http_client: ExAws.Request.HttpMock, + json_codec: JSX, + access_key_id: "AKIAIOSFODNN7EXAMPLE", + secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + region: "us-east-1", + retries: [ + max_attempts: 5, + base_backoff_in_ms: 1, + max_backoff_in_ms: 20 + ], + normalize_path: false, + scheme: "https://", + host: "s3.amazonaws.com", + virtual_host: true, + port: 443 + } + + headers = %{ + "x-amz-bucket-region" => "us-east-1", + "x-amz-content-sha256" => ExAws.Auth.Utils.hash_sha256(""), + "content-length" => byte_size("") + } + + expect( + ExAws.Request.HttpMock, + :request, + fn _method, url, _body, _headers, _opts -> + assert url == + "https://examplebucket.s3.amazonaws.com/test%20hello%20%233.txt%3Facl%3D21?versionId=v1" + + {:ok, %{status_code: 200}} + end + ) + + operation = %ExAws.Operation.S3{ + s3_operation() + | path: "test hello #3.txt?acl=21", + headers: headers, + bucket: "examplebucket", + params: %{"versionId" => "v1"} + } + + assert {:ok, %{status_code: 200}} == S3.perform(operation, config) + end + + describe "encode_path_query_fragment_into_path" do + test "fetch object ending in question mark (?)" do + %{path: "object.txt%3F"} = + %ExAws.Operation.S3{path: "object.txt?"} + |> S3.encode_path_query_fragment_into_path() + end + + test "fetch object with name contains query parameters" do + %{path: "object.txt%3Fid%3D3"} = + %ExAws.Operation.S3{path: "object.txt?id=3"} + |> S3.encode_path_query_fragment_into_path() + end + end end diff --git a/test/ex_aws/request_test.exs b/test/ex_aws/request_test.exs index 7cf8a08d..161bc92d 100644 --- a/test/ex_aws/request_test.exs +++ b/test/ex_aws/request_test.exs @@ -65,6 +65,8 @@ defmodule ExAws.RequestTest do }} end + # URL encoding is done under ExAws.Operation.S3 + @tag :skip test "handles encoding S3 URLs with params", context do http_method = :get url = "https://examplebucket.s3.amazonaws.com/test hello #3.txt?acl=21" @@ -109,6 +111,8 @@ defmodule ExAws.RequestTest do }} end + # URL encoding is done under ExAws.Operation.S3 + @tag :skip test "handles encoding S3 URLs without params", context do http_method = :get url = "https://examplebucket.s3.amazonaws.com/up//double//test hello+#3.txt"