Skip to content

Commit

Permalink
Feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmj committed Feb 12, 2024
1 parent 6f57d4b commit 17d2697
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 92 deletions.
148 changes: 75 additions & 73 deletions lib/mint/core/headers.ex
Original file line number Diff line number Diff line change
@@ -1,133 +1,135 @@
defmodule Mint.Core.Headers do
@type canonical_header() ::
@moduledoc false

@type canonical() ::
{original_name :: String.t(), canonical_name :: String.t(), value :: String.t()}
@type raw_header() :: {original_name :: String.t(), value :: String.t()}

@unallowed_trailer_headers MapSet.new([
"content-encoding",
"content-length",
"content-range",
"content-type",
"trailer",
"transfer-encoding",

# Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1)
"cache-control",
"expect",
"host",
"max-forwards",
"pragma",
"range",
"te",

# Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2)
"if-match",
"if-none-match",
"if-modified-since",
"if-unmodified-since",
"if-range",

# Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3)
"authorization",
"proxy-authenticate",
"proxy-authorization",
"www-authenticate",

# Cookie management (https://tools.ietf.org/html/rfc6265)
"cookie",
"set-cookie",

# Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1)
"age",
"cache-control",
"expires",
"date",
"location",
"retry-after",
"vary",
"warning"
])

@spec from_raw_headers([raw_header()]) :: [canonical_header()]
def from_raw_headers(headers) do
Enum.map(headers, fn {name, value} -> {name, String.downcase(name, :ascii), value} end)
@type raw() :: {original_name :: String.t(), value :: String.t()}

@unallowed_trailers MapSet.new([
"content-encoding",
"content-length",
"content-range",
"content-type",
"trailer",
"transfer-encoding",

# Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1)
"cache-control",
"expect",
"host",
"max-forwards",
"pragma",
"range",
"te",

# Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2)
"if-match",
"if-none-match",
"if-modified-since",
"if-unmodified-since",
"if-range",

# Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3)
"authorization",
"proxy-authenticate",
"proxy-authorization",
"www-authenticate",

# Cookie management (https://tools.ietf.org/html/rfc6265)
"cookie",
"set-cookie",

# Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1)
"age",
"cache-control",
"expires",
"date",
"location",
"retry-after",
"vary",
"warning"
])

@spec from_raw([raw()]) :: [canonical()]
def from_raw(headers) do
Enum.map(headers, fn {name, value} -> {name, lower_raw(name), value} end)
end

@spec to_raw_headers([canonical_header()], boolean()) :: [raw_header()]
def to_raw_headers(headers, _case_sensitive_headers = true) do
@spec to_raw([canonical()], boolean()) :: [raw()]
def to_raw(headers, _case_sensitive = true) do
Enum.map(headers, fn {name, _canonical_name, value} -> {name, value} end)
end

def to_raw_headers(headers, _case_sensitive_headers = false) do
def to_raw(headers, _case_sensitive = false) do
Enum.map(headers, fn {_name, canonical_name, value} ->
{canonical_name, value}
end)
end

@spec find_header([canonical_header()], String.t()) :: {String.t(), String.t()} | nil
def find_header(headers, name) do
@spec find([canonical()], String.t()) :: {String.t(), String.t()} | nil
def find(headers, name) do
case List.keyfind(headers, name, 1) do
nil -> nil
{name, _canonical_name, value} -> {name, value}
end
end

@spec replace_header([canonical_header()], String.t(), String.t(), String.t()) ::
[canonical_header()]
def replace_header(headers, new_name, canonical_name, value) do
@spec replace([canonical()], String.t(), String.t(), String.t()) ::
[canonical()]
def replace(headers, new_name, canonical_name, value) do
List.keyreplace(headers, canonical_name, 1, {new_name, canonical_name, value})
end

@spec has_header?([canonical_header()], String.t()) :: boolean()
def has_header?(headers, name) do
@spec has?([canonical()], String.t()) :: boolean()
def has?(headers, name) do
List.keymember?(headers, name, 1)
end

@spec put_new_header([canonical_header()], String.t(), String.t(), String.t() | nil) ::
[canonical_header()]
def put_new_header(headers, _name, _canonical_name, nil) do
@spec put_new([canonical()], String.t(), String.t(), String.t() | nil) ::
[canonical()]
def put_new(headers, _name, _canonical_name, nil) do
headers
end

def put_new_header(headers, name, canonical_name, value) do
def put_new(headers, name, canonical_name, value) do
if List.keymember?(headers, canonical_name, 1) do
headers
else
[{name, canonical_name, value} | headers]
end
end

@spec put_new_header([canonical_header()], String.t(), String.t(), (-> String.t())) ::
[canonical_header()]
def put_new_header_lazy(headers, name, canonical_name, fun) do
@spec put_new([canonical()], String.t(), String.t(), (-> String.t())) ::
[canonical()]
def put_new_lazy(headers, name, canonical_name, fun) do
if List.keymember?(headers, canonical_name, 1) do
headers
else
[{name, canonical_name, fun.()} | headers]
end
end

@spec find_unallowed_trailer([canonical_header()]) :: String.t() | nil
@spec find_unallowed_trailer([canonical()]) :: String.t() | nil
def find_unallowed_trailer(headers) do
Enum.find_value(headers, fn
{raw_name, canonical_name, _value} ->
if canonical_name in @unallowed_trailer_headers do
if canonical_name in @unallowed_trailers do
raw_name
end
end)
end

@spec remove_unallowed_trailer([raw_header()]) :: [raw_header()]
@spec remove_unallowed_trailer([raw()]) :: [raw()]
def remove_unallowed_trailer(headers) do
Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailer_headers end)
Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailers end)
end

@spec lower_raw(String.t()) :: String.t()
def lower_raw(name) do
String.downcase(name, :ascii)
end

@spec lower_raws([raw_header()]) :: [raw_header()]
@spec lower_raws([raw()]) :: [raw()]
def lower_raws(headers) do
Enum.map(headers, fn {name, value} -> {lower_raw(name), value} end)
end
Expand Down
30 changes: 15 additions & 15 deletions lib/mint/http1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ defmodule Mint.HTTP1 do
## Additional Options
* `:case_sensitive_headers` - (boolean) if set to true the case of the supplied
* `:case_sensitive_headers` - (boolean) if set to `true` the case of the supplied
headers in requests will be preserved. The default is to lowercase the headers
because http/1.1 header names are not case-insensitive.
because HTTP/1.1 header names are case-insensitive.
"""
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
Expand Down Expand Up @@ -270,15 +270,15 @@ defmodule Mint.HTTP1 do

headers =
headers
|> Headers.from_raw_headers()
|> Headers.from_raw()
|> add_default_headers(conn)

with {:ok, headers, encoding} <- add_content_length_or_transfer_encoding(headers, body),
{:ok, iodata} <-
Request.encode(
method,
path,
Headers.to_raw_headers(headers, conn.case_sensitive_headers),
Headers.to_raw(headers, conn.case_sensitive_headers),
body
),
:ok <- transport.send(socket, iodata) do
Expand Down Expand Up @@ -405,13 +405,13 @@ defmodule Mint.HTTP1 do
end
end

defp validate_chunk(conn, {:eof, trailer_headers}) do
headers = Headers.from_raw_headers(trailer_headers)
defp validate_chunk(conn, {:eof, trailers}) do
trailers = Headers.from_raw(trailers)

if unallowed_header = Headers.find_unallowed_trailer(headers) do
if unallowed_header = Headers.find_unallowed_trailer(trailers) do
{:error, wrap_error({:unallowed_trailing_header, unallowed_header})}
else
{:ok, {:eof, Headers.to_raw_headers(headers, conn.case_sensitive_headers)}}
{:ok, {:eof, Headers.to_raw(trailers, conn.case_sensitive_headers)}}
end
end

Expand Down Expand Up @@ -988,8 +988,8 @@ defmodule Mint.HTTP1 do

defp add_default_headers(headers, conn) do
headers
|> Headers.put_new_header("User-Agent", "user-agent", @user_agent)
|> Headers.put_new_header("Host", "host", default_host_header(conn))
|> Headers.put_new("User-Agent", "user-agent", @user_agent)
|> Headers.put_new("Host", "host", default_host_header(conn))
end

# If the port is the default for the scheme, don't add it to the host header
Expand All @@ -1003,18 +1003,18 @@ defmodule Mint.HTTP1 do

defp add_content_length_or_transfer_encoding(headers, :stream) do
cond do
Headers.has_header?(headers, "content-length") ->
Headers.has?(headers, "content-length") ->
{:ok, headers, :identity}

found = Headers.find_header(headers, "transfer-encoding") ->
found = Headers.find(headers, "transfer-encoding") ->
{raw_name, value} = found

with {:ok, tokens} <- Parse.transfer_encoding_header(value) do
if "chunked" in tokens or "identity" in tokens do
{:ok, headers, :identity}
else
headers =
Headers.replace_header(headers, raw_name, "transfer-encoding", value <> ",chunked")
Headers.replace(headers, raw_name, "transfer-encoding", value <> ",chunked")

{:ok, headers, :chunked}
end
Expand All @@ -1024,7 +1024,7 @@ defmodule Mint.HTTP1 do
# chunked transfer-encoding and handle the encoding ourselves.
true ->
headers =
Headers.put_new_header(headers, "Transfer-Encoding", "transfer-encoding", "chunked")
Headers.put_new(headers, "Transfer-Encoding", "transfer-encoding", "chunked")

{:ok, headers, :chunked}
end
Expand All @@ -1037,7 +1037,7 @@ defmodule Mint.HTTP1 do
defp add_content_length_or_transfer_encoding(headers, body) do
length_fun = fn -> body |> IO.iodata_length() |> Integer.to_string() end

{:ok, Headers.put_new_header_lazy(headers, "Content-Length", "content-length", length_fun),
{:ok, Headers.put_new_lazy(headers, "Content-Length", "content-length", length_fun),
:identity}
end

Expand Down
8 changes: 4 additions & 4 deletions lib/mint/http2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1106,15 +1106,15 @@ defmodule Mint.HTTP2 do
encode_data(conn, stream_id, "", [:end_stream])
end

defp encode_stream_body_request_payload(conn, stream_id, {:eof, trailer_headers}) do
trailer_headers = Headers.from_raw_headers(trailer_headers)
defp encode_stream_body_request_payload(conn, stream_id, {:eof, trailers}) do
trailers = Headers.from_raw(trailers)

if unallowed_trailer_header = Headers.find_unallowed_trailer(trailer_headers) do
if unallowed_trailer_header = Headers.find_unallowed_trailer(trailers) do
error = wrap_error({:unallowed_trailing_header, unallowed_trailer_header})
throw({:mint, conn, error})
end

trailer_headers = Headers.to_raw_headers(trailer_headers, _case_sensitive_headers = false)
trailer_headers = Headers.to_raw(trailers, _case_sensitive = false)
encode_headers(conn, stream_id, trailer_headers, [:end_headers, :end_stream])
end

Expand Down

0 comments on commit 17d2697

Please sign in to comment.