Packages
bandit
1.0.0-pre.16
1.12.1
1.12.0
1.11.1
1.11.0
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
retired
1.9.0
1.8.0
1.7.0
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.0
1.2.3
1.2.2
1.2.1
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-pre.18
1.0.0-pre.17
1.0.0-pre.16
1.0.0-pre.15
1.0.0-pre.14
1.0.0-pre.13
1.0.0-pre.12
1.0.0-pre.11
1.0.0-pre.10
1.0.0-pre.9
1.0.0-pre.8
1.0.0-pre.7
1.0.0-pre.6
1.0.0-pre.5
1.0.0-pre.4
1.0.0-pre.3
1.0.0-pre.2
1.0.0-pre.1
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.2.3
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
A pure-Elixir HTTP server built for Plug & WebSock apps
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/bandit/http2/stream_task.ex
defmodule Bandit.HTTP2.StreamTask do
@moduledoc false
# This Task is where an actual Plug is executed, within the context of an HTTP/2 stream. There
# is a bit of split responsibility between this module and the `Bandit.HTTP2.Adapter` module
# which merits explanation:
#
# Broadly, this module is responsible for the execution of a Plug and does so within a Task
# process. Task is used in preference to GenServer here because of the shape of the
# `Plug.Conn.Adapter` API (implemented by the `Bandit.HTTP2.Adapter` module). Specifically, that
# API requires blocking semantics for the `Plug.Conn.Adapter.read_req_body/2`) call and expects
# it to block until some underlying condition has been met (the body has been read, a timeout
# has occurred, etc). The events which 'unblock' these conditions typically come from within the
# Connection, and are pushed down to streams as a fundamental design decision (rather than
# having stream processes query the connection directly). As such, it is much simpler for Task
# processes to wait in an imperative fashion using `receive` calls directly.
#
# To contain these design decisions, the 'connection-facing' API for sending data to a stream
# process is expressed on this module (via the `recv_*` functions) even though the 'other half'
# of those calls exists in the `Bandit.HTTP2.Adapter` module. As a result, this module and the
# Handler module are fairly tightly coupled, but together they express clear APIs towards both
# Plug applications and the rest of Bandit.
use Task
# A stream process can be created only once we have an adapter & set of headers. Pass them in
# at creation time to ensure this invariant
@spec start_link(
Bandit.HTTP2.Adapter.t(),
Bandit.TransportInfo.t(),
Plug.Conn.headers(),
Bandit.Pipeline.plug_def(),
Bandit.Telemetry.t()
) :: {:ok, pid()}
def start_link(req, transport_info, headers, plug, span) do
Task.start_link(__MODULE__, :run, [req, transport_info, headers, plug, span])
end
# Let the stream task know that body data has arrived from the client. The other half of this
# flow can be found in `Bandit.HTTP2.Adapter.read_req_body/2`
@spec recv_data(pid(), iodata()) :: :ok | :noconnect | :nosuspend
def recv_data(pid, data), do: send(pid, {:data, data})
# Let the stream task know that the client has set the end of stream flag. The other half of
# this flow can be found in `Bandit.HTTP2.Adapter.read_req_body/2`
@spec recv_end_of_stream(pid()) :: :ok | :noconnect | :nosuspend
def recv_end_of_stream(pid), do: send(pid, :end_stream)
# Let the stream task know that the client has reset the stream. This will terminate the
# stream's handling process
@spec recv_rst_stream(pid(), Bandit.HTTP2.Errors.error_code()) :: true
def recv_rst_stream(pid, error_code), do: Process.exit(pid, {:recv_rst_stream, error_code})
def run(req, transport_info, all_headers, plug, span) do
with {:ok, request_target} <- build_request_target(all_headers),
method <- Bandit.Headers.get_header(all_headers, ":method") do
with {:ok, pseudo_headers, headers} <- split_headers(all_headers),
:ok <- pseudo_headers_all_request(pseudo_headers),
:ok <- exactly_one_instance_of(pseudo_headers, ":scheme"),
:ok <- exactly_one_instance_of(pseudo_headers, ":method"),
:ok <- exactly_one_instance_of(pseudo_headers, ":path"),
:ok <- headers_all_lowercase(headers),
:ok <- no_connection_headers(headers),
:ok <- valid_te_header(headers),
headers <- combine_cookie_crumbs(headers),
req <- Bandit.HTTP2.Adapter.add_end_header_metric(req),
adapter <- {Bandit.HTTP2.Adapter, req},
{:ok, %Plug.Conn{adapter: {Bandit.HTTP2.Adapter, req}} = conn} <-
Bandit.Pipeline.run(adapter, transport_info, method, request_target, headers, plug) do
Bandit.Telemetry.stop_span(span, req.metrics, %{
conn: conn,
method: method,
request_target: request_target,
status: conn.status
})
:ok
else
{:error, reason} ->
raise Bandit.HTTP2.Stream.StreamError,
message: reason,
method: method,
request_target: request_target
end
else
{:error, reason} -> raise Bandit.HTTP2.Stream.StreamError, reason
end
end
defp build_request_target(headers) do
with scheme <- Bandit.Headers.get_header(headers, ":scheme"),
{:ok, host, port} <- get_host_and_port(headers),
{:ok, path} <- get_path(headers) do
{:ok, {scheme, host, port, path}}
end
end
defp get_host_and_port(headers) do
case Bandit.Headers.get_header(headers, ":authority") do
authority when not is_nil(authority) -> Bandit.Headers.parse_hostlike_header(authority)
nil -> {:ok, nil, nil}
end
end
# RFC9113§8.3.1 - path should be non-empty and absolute
defp get_path(headers) do
headers
|> Bandit.Headers.get_header(":path")
|> case do
nil -> {:error, "Received empty :path"}
"*" -> {:ok, :*}
"/" <> _ = path -> split_path(path)
_ -> {:error, "Path does not start with /"}
end
end
# RFC9113§8.3.1 - path should match the path-absolute production from RFC3986
defp split_path(path) do
if path |> String.split("/") |> Enum.all?(&(&1 not in [".", ".."])),
do: {:ok, path},
else: {:error, "Path contains dot segment"}
end
# RFC9113§8.3 - pseudo headers must appear first
defp split_headers(headers) do
{pseudo_headers, headers} =
Enum.split_while(headers, fn {key, _value} -> String.starts_with?(key, ":") end)
if Enum.any?(headers, fn {key, _value} -> String.starts_with?(key, ":") end),
do: {:error, "Received pseudo headers after regular one"},
else: {:ok, pseudo_headers, headers}
end
# RFC9113§8.3.1 - only request pseudo headers may appear
defp pseudo_headers_all_request(headers) do
if Enum.any?(headers, fn {key, _value} -> key not in ~w[:method :scheme :authority :path] end),
do: {:error, "Received invalid pseudo header"},
else: :ok
end
# RFC9113§8.3.1 - method, scheme, path pseudo headers must appear exactly once
defp exactly_one_instance_of(headers, header) do
headers
|> Enum.count(fn {key, _value} -> key == header end)
|> case do
1 -> :ok
_ -> {:error, "Expected 1 #{header} headers"}
end
end
# RFC9113§8.2 - all headers name fields must be lowercsae
defp headers_all_lowercase(headers) do
if Enum.all?(headers, fn {key, _value} -> lowercase?(key) end),
do: :ok,
else: {:error, "Received uppercase header"}
end
defp lowercase?(<<char, _rest::bits>>) when char >= ?A and char <= ?Z, do: false
defp lowercase?(<<_char, rest::bits>>), do: lowercase?(rest)
defp lowercase?(<<>>), do: true
# RFC9113§8.2.2 - no hop-by-hop headers
# Note that we do not filter out the TE header here, since it is allowed in
# specific cases by RFC9113§8.2.2. We check those cases in a separate filter
defp no_connection_headers(headers) do
connection_headers =
~w[connection keep-alive proxy-authenticate proxy-authorization trailers transfer-encoding upgrade]
if Enum.any?(headers, fn {key, _value} -> key in connection_headers end),
do: {:error, "Received connection-specific header"},
else: :ok
end
# RFC9113§8.2.2 - TE header may be present if it contains exactly 'trailers'
defp valid_te_header(headers) do
if Bandit.Headers.get_header(headers, "te") in [nil, "trailers"],
do: :ok,
else: {:error, "Received invalid TE header"}
end
# Per RFC9113§8.2.3
defp combine_cookie_crumbs(headers) do
{crumbs, other_headers} = headers |> Enum.split_with(fn {header, _} -> header == "cookie" end)
combined_cookie = Enum.map_join(crumbs, "; ", fn {"cookie", crumb} -> crumb end)
[{"cookie", combined_cookie} | other_headers]
end
end