Packages

OCI/Docker image metadata validator and resolver for Elixir

Current section

Files

Jump to
regc lib regc oci transport http request_encoder.ex
Raw

lib/regc/oci/transport/http/request_encoder.ex

defmodule Regc.Oci.Transport.Http.RequestEncoder do
@moduledoc false
alias Regc.Oci.Transport.Request
@unsafe_request_target ~r/[\x00-\x20\x7F]/
@spec parse_url(String.t()) :: {:ok, URI.t()} | {:error, term()}
def parse_url(url) do
uri = URI.parse(url)
if valid_uri?(uri) do
{:ok, uri}
else
{:error, {:invalid_url, url}}
end
rescue
_cause -> {:error, {:invalid_url, url}}
end
@spec valid_uri?(URI.t()) :: boolean()
def valid_uri?(%URI{} = uri) do
uri.scheme in ["http", "https"] and is_binary(uri.host) and uri.host != "" and
is_nil(uri.userinfo) and valid_port?(uri.port) and safe_request_target?(uri)
end
@spec encode(Request.t(), URI.t()) :: iodata()
def encode(%Request{} = request, %URI{} = uri) do
headers =
request.headers
|> Enum.reject(fn {name, _value} ->
String.downcase(to_string(name)) in ["host", "connection"]
end)
|> Enum.concat([{"host", host_header(uri)}, {"connection", "close"}])
[
"GET ",
request_target(uri),
" HTTP/1.1\r\n",
Enum.map(headers, fn {name, value} ->
[header_name(name), ": ", header_value(value), "\r\n"]
end),
"\r\n"
]
end
defp valid_port?(port), do: is_integer(port) and port in 1..65_535
defp safe_request_target?(%URI{path: path, query: query}) do
Enum.all?([path, query], fn
nil -> true
component -> not Regex.match?(@unsafe_request_target, component)
end)
end
defp header_name(value) do
value = to_string(value)
if Regex.match?(~r/\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/, value) do
value
else
raise ArgumentError, "HTTP header name is invalid"
end
end
defp header_value(value) do
value = to_string(value)
if valid_header_value?(value),
do: value,
else: raise(ArgumentError, "HTTP header value is invalid")
end
defp valid_header_value?(value) do
value
|> :binary.bin_to_list()
|> Enum.all?(fn byte ->
byte == 0x09 or byte in 0x20..0x7E or byte >= 0x80
end)
end
defp host_header(%URI{host: host} = uri) do
host = if String.contains?(host, ":"), do: "[#{host}]", else: host
if uri.port == default_port(uri.scheme),
do: host,
else: "#{host}:#{uri.port}"
end
defp request_target(%URI{path: path, query: query}) do
path = if path in [nil, ""], do: "/", else: path
if is_binary(query), do: path <> "?" <> query, else: path
end
defp default_port("https"), do: 443
defp default_port("http"), do: 80
end