defmodule ExSecrets.AwsSecretsManager.Request.Url do @moduledoc false @doc """ Builds URL for an operation and a config" """ def build(operation, config) do config |> Map.take([:scheme, :host, :port]) |> Map.put(:query, query(operation)) |> Map.put(:path, operation.path) |> normalize_scheme |> normalize_path(config.normalize_path) |> convert_port_to_integer |> (&struct(URI, &1)).() |> URI.to_string() |> String.trim_trailing("?") end defp query(operation) do operation |> Map.get(:params, %{}) |> normalize_params |> URI.encode_query() end defp normalize_scheme(url) do url |> Map.update(:scheme, "", &String.replace(&1, "://", "")) end defp normalize_path(url, false), do: url defp normalize_path(url, _normalize) do url |> Map.update(:path, "", &String.replace(&1, ~r/\/{2,}/, "/")) end defp convert_port_to_integer(url = %{port: port}) when is_binary(port) do {port, _} = Integer.parse(port) put_in(url[:port], port) end defp convert_port_to_integer(url), do: url defp normalize_params(params) when is_map(params) do params |> Map.delete("") |> Map.delete(nil) end defp normalize_params(params), do: params def sanitize(url, service) when service in ["s3", :s3] do new_path = url |> get_path(service) |> String.replace_prefix("/", "") |> uri_encode() query = case String.split(url, "?", parts: 2) do [_] -> nil [_, ""] -> nil [_, q] -> q end url |> URI.parse() |> Map.put(:fragment, nil) |> Map.put(:path, "/" <> new_path) |> Map.put(:query, query) |> URI.to_string() |> String.replace("+", "%20") end def sanitize(url, _), do: String.replace(url, "+", "%20") def get_path(url, service \\ nil) # Elixir's URI.parse will treat everything after the # sign # as a fragment. This is correct, but S3 treats it as part # of the path of the object. # # This will split the URL at the base with the right side # being the path, except for the query params. # # for example: # `"https://bucket.aws.com/my/path/here+ #3.txt?t=21"` # https://bucket.aws.com | /my/path/here+ #3.txt?t=21 # ________base__________ | /my/path/here+ #3.txt | t=21 # ________base__________ | /my/path/here+ #3.txt | _params_ # # After using `uri_encode` it will be `/my/path/here%2B%20%233.txt` # def get_path(url, service) when service in ["s3", :s3] do base = url |> URI.parse() |> Map.put(:path, nil) |> Map.put(:query, nil) |> Map.put(:fragment, nil) |> URI.to_string() [_base, path_with_params] = String.split(url, base, parts: 2) [path | _query_params] = String.split(path_with_params, "?", parts: 2) path end def get_path(url, _), do: URI.parse(url).path || "/" def uri_encode(url), do: URI.encode(url, &valid_path_char?/1) # Space character defp valid_path_char?(?\s), do: false defp valid_path_char?(?/), do: true defp valid_path_char?(c) do URI.char_unescaped?(c) && !URI.char_reserved?(c) end end