Packages
bandit
0.4.2
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.ex
defmodule Bandit.HTTP2.Stream do
@moduledoc false
# Carries out state management transitions per RFC7540§5.1. Anything having to do
# with the internal state of a stream is handled in this module. Note that sending
# of frames on behalf of a stream is a bit of a split responsibility: the stream
# itself may update state depending on the value of the end_stream flag (this is
# a stream concern and thus handled here), but the sending of the data over the
# wire is a connection concern as it must be serialized properly & is subject to
# flow control at a connection level
require Integer
require Logger
alias Bandit.HTTP2.{Connection, Errors, FlowControl, StreamTask}
defstruct stream_id: nil,
state: nil,
pid: nil,
recv_window_size: nil,
send_window_size: nil,
pending_content_length: nil
@typedoc "An HTTP/2 stream identifier"
@type stream_id :: non_neg_integer()
@typedoc "An HTTP/2 stream state"
@type state :: :reserved_local | :idle | :open | :local_closed | :remote_closed | :closed
@typedoc "A description of a stream error"
@type error :: {:stream, stream_id(), Errors.error_code(), String.t()}
@typedoc "A single HTTP/2 stream"
@type t :: %__MODULE__{
stream_id: stream_id(),
state: state(),
pid: pid() | nil,
recv_window_size: non_neg_integer(),
send_window_size: non_neg_integer(),
pending_content_length: non_neg_integer() | nil
}
@spec recv_headers(
t(),
Plug.Conn.headers(),
boolean,
Plug.Conn.Adapter.peer_data(),
Bandit.plug()
) :: {:ok, t()} | {:error, Connection.error()} | {:error, error()}
def recv_headers(%__MODULE__{state: state} = stream, trailers, true, _peer, _plug)
when state in [:open, :local_closed] do
with :ok <- no_pseudo_headers(trailers, stream.stream_id) do
# These are actually trailers, which Plug doesn't support. Log and ignore
Logger.warn("Ignoring trailers on stream #{stream.stream_id}: #{inspect(trailers)}")
{:ok, stream}
end
end
def recv_headers(%__MODULE__{state: :idle} = stream, headers, _end_stream, peer, plug) do
with :ok <- stream_id_is_valid_client(stream.stream_id),
:ok <- headers_all_lowercase(headers, stream.stream_id),
:ok <- pseudo_headers_all_request(headers, stream.stream_id),
:ok <- pseudo_headers_first(headers, stream.stream_id),
:ok <- no_connection_headers(headers, stream.stream_id),
:ok <- valid_te_header(headers, stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":scheme", stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":method", stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":path", stream.stream_id),
:ok <- non_empty_path(headers, stream.stream_id),
expected_content_length <- expected_content_length(headers) do
{:ok, pid} = StreamTask.start_link(self(), stream.stream_id, headers, peer, plug)
{:ok, %{stream | state: :open, pid: pid, pending_content_length: expected_content_length}}
end
end
def recv_headers(%__MODULE__{}, _headers, _end_stream, _peer, _plug) do
{:error, {:connection, Errors.protocol_error(), "Received HEADERS in unexpected state"}}
end
@spec send_push_headers(t(), Plug.Conn.headers()) :: {:ok, t()} | {:error, error}
def send_push_headers(%__MODULE__{state: :idle} = stream, headers) do
with :ok <- stream_id_is_valid_server(stream.stream_id),
:ok <- headers_all_lowercase(headers, stream.stream_id),
:ok <- pseudo_headers_all_request(headers, stream.stream_id),
:ok <- pseudo_headers_first(headers, stream.stream_id),
:ok <- no_connection_headers(headers, stream.stream_id),
:ok <- valid_te_header(headers, stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":scheme", stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":method", stream.stream_id),
:ok <- exactly_one_instance_of(headers, ":path", stream.stream_id),
:ok <- non_empty_path(headers, stream.stream_id) do
{:ok, %{stream | state: :reserved_local}}
else
{:error, {:connection, _error_code, error_message}} -> {:error, error_message}
{:error, {:stream, _stream_id, _error_code, error_message}} -> {:error, error_message}
end
end
@spec start_push(t(), Plug.Conn.headers(), Plug.Conn.Adapter.peer_data(), Bandit.plug()) ::
{:ok, t()}
def start_push(%__MODULE__{state: :reserved_local} = stream, headers, peer, plug) do
{:ok, pid} = StreamTask.start_link(self(), stream.stream_id, headers, peer, plug)
{:ok, %{stream | state: :remote_closed, pid: pid}}
end
# RFC7540§5.1.1 - client initiated streams must be odd
defp stream_id_is_valid_client(stream_id) do
if Integer.is_odd(stream_id) do
:ok
else
{:error, {:connection, Errors.protocol_error(), "Received HEADERS with even stream_id"}}
end
end
# RFC7540§5.1.1 - server initiated streams must be even
defp stream_id_is_valid_server(stream_id) do
if Integer.is_even(stream_id) do
:ok
else
{:error, {:connection, Errors.protocol_error(), "Sending HEADERS with odd stream_id"}}
end
end
# RFC7540§8.1.2 - all headers name fields must be lowercsae
defp headers_all_lowercase(headers, stream_id) do
headers
|> Enum.all?(fn {key, _value} -> String.downcase(key) == key end)
|> if do
:ok
else
{:error, {:stream, stream_id, Errors.protocol_error(), "Received uppercase header"}}
end
end
# RFC7540§8.1.2.1 - only request pseudo headers may appear
defp pseudo_headers_all_request(headers, stream_id) do
headers
|> Enum.all?(fn
{":" <> key, _value} -> key in ~w[method scheme authority path]
{_key, _value} -> true
end)
|> if do
:ok
else
{:error, {:stream, stream_id, Errors.protocol_error(), "Received invalid pseudo header"}}
end
end
# RFC7540§8.1.2.1 - pseudo headers must appear first
defp no_pseudo_headers(headers, stream_id) do
headers
|> Enum.any?(fn {key, _value} -> String.starts_with?(key, ":") end)
|> if do
{:error,
{:stream, stream_id, Errors.protocol_error(), "Received trailers with pseudo headers"}}
else
:ok
end
end
# RFC7540§8.1.2.2 - pseudo headers must appear first
defp pseudo_headers_first(headers, stream_id) do
headers
|> Enum.drop_while(fn {key, _value} -> String.starts_with?(key, ":") end)
|> Enum.any?(fn {key, _value} -> String.starts_with?(key, ":") end)
|> if do
{:error,
{:stream, stream_id, Errors.protocol_error(), "Received pseudo headers after regular one"}}
else
:ok
end
end
# RFC7540§8.1.2.2 - no hop-by-hop headers from RFC2616§13.5.1
# Note that we do not filter out the TE header here, since it is allowed in
# specific cases by RFC7540§8.1.2.2. We check those cases in a separate filter
defp no_connection_headers(headers, stream_id) do
headers
|> Enum.any?(fn {key, _value} ->
key in ~w[connection keep-alive proxy-authenticate proxy-authorization trailers transfer-encoding upgrade]
end)
|> if do
{:error,
{:stream, stream_id, Errors.protocol_error(), "Received connection-specific header"}}
else
:ok
end
end
# RFC7540§8.1.2.2 - TE header may be present if it contains exactly 'trailers'
defp valid_te_header(headers, stream_id) do
case List.keyfind(headers, "te", 0) do
nil ->
:ok
{_, "trailers"} ->
:ok
_ ->
{:error, {:stream, stream_id, Errors.protocol_error(), "Received invalid TE header"}}
end
end
# RFC7540§8.1.2.3 - method, scheme, path pseudo headers must appear exactly once
defp exactly_one_instance_of(headers, header, stream_id) do
headers
|> Enum.count(fn {key, _value} -> key == header end)
|> case do
1 ->
:ok
_ ->
{:error, {:stream, stream_id, Errors.protocol_error(), "Expected 1 #{header} headers"}}
end
end
# RFC7540§8.1.2.3 :path must not be empty
defp non_empty_path(headers, stream_id) do
case List.keyfind(headers, ":path", 0) do
{_, ""} ->
{:error, {:stream, stream_id, Errors.protocol_error(), "Received empty :path"}}
_ ->
:ok
end
end
defp expected_content_length(headers) do
case List.keyfind(headers, "content-length", 0) do
nil -> nil
{_, content_length} -> String.to_integer(content_length)
end
end
@spec recv_data(t(), binary()) :: {:ok, t(), non_neg_integer()} | {:error, Connection.error()}
def recv_data(%__MODULE__{state: state} = stream, data) when state in [:open, :local_closed] do
StreamTask.recv_data(stream.pid, data)
{new_window, increment} =
FlowControl.compute_recv_window(stream.recv_window_size, byte_size(data))
pending_content_length =
case stream.pending_content_length do
nil -> nil
pending_content_length -> pending_content_length - byte_size(data)
end
{:ok,
%{stream | recv_window_size: new_window, pending_content_length: pending_content_length},
increment}
end
def recv_data(%__MODULE__{} = stream, _data) do
{:error, {:connection, Errors.protocol_error(), "Received DATA when in #{stream.state}"}}
end
@spec recv_window_update(t(), non_neg_integer()) ::
{:ok, t()} | {:error, Connection.error()} | {:error, error()}
def recv_window_update(%__MODULE__{state: :idle}, _increment) do
{:error, {:connection, Errors.protocol_error(), "Received WINDOW_UPDATE when in idle"}}
end
def recv_window_update(%__MODULE__{} = stream, increment) do
case FlowControl.update_send_window(stream.send_window_size, increment) do
{:ok, new_window} ->
{:ok, %{stream | send_window_size: new_window}}
{:error, error} ->
{:error, {:stream, stream.stream_id, Errors.flow_control_error(), error}}
end
end
@spec recv_rst_stream(t(), Errors.error_code()) ::
{:ok, t()} | {:error, Connection.error()}
def recv_rst_stream(%__MODULE__{state: :idle}, _error_code) do
{:error, {:connection, Errors.protocol_error(), "Received RST_STREAM when in idle"}}
end
def recv_rst_stream(%__MODULE__{} = stream, error_code) do
if is_pid(stream.pid), do: StreamTask.recv_rst_stream(stream.pid, error_code)
{:ok, %{stream | state: :closed, pid: nil}}
end
@spec recv_end_of_stream(t(), boolean()) ::
{:ok, t()} | {:error, Connection.error()}
def recv_end_of_stream(%__MODULE__{state: :open} = stream, true) do
with :ok <- verify_content_length(stream) do
StreamTask.recv_end_of_stream(stream.pid)
{:ok, %{stream | state: :remote_closed}}
end
end
def recv_end_of_stream(%__MODULE__{state: :local_closed} = stream, true) do
with :ok <- verify_content_length(stream) do
StreamTask.recv_end_of_stream(stream.pid)
{:ok, %{stream | state: :closed, pid: nil}}
end
end
def recv_end_of_stream(%__MODULE__{}, true) do
{:error, {:connection, Errors.protocol_error(), "Received unexpected end_stream"}}
end
def recv_end_of_stream(%__MODULE__{} = stream, false) do
{:ok, stream}
end
defp verify_content_length(%__MODULE__{pending_content_length: nil}), do: :ok
defp verify_content_length(%__MODULE__{pending_content_length: 0}), do: :ok
defp verify_content_length(%__MODULE__{} = stream) do
{:error,
{:stream, stream.stream_id, Errors.protocol_error(),
"Received end of stream with #{stream.pending_content_length} byte(s) pending"}}
end
@spec owner?(t(), pid()) :: :ok | {:error, :not_owner}
def owner?(%__MODULE__{pid: pid}, pid), do: :ok
def owner?(%__MODULE__{}, _pid), do: {:error, :not_owner}
@spec get_send_window_size(t()) :: non_neg_integer()
def get_send_window_size(%__MODULE__{} = stream), do: stream.send_window_size
@spec send_headers(t()) :: {:ok, t()} | {:error, :invalid_state}
def send_headers(%__MODULE__{state: state} = stream) when state in [:open, :remote_closed] do
{:ok, stream}
end
def send_headers(%__MODULE__{}) do
{:error, :invalid_state}
end
@spec send_data(t(), non_neg_integer()) ::
{:ok, t()} | {:error, :insufficient_window_size} | {:error, :invalid_state}
def send_data(%__MODULE__{state: state} = stream, 0) when state in [:open, :remote_closed] do
{:ok, stream}
end
def send_data(%__MODULE__{state: state} = stream, len) when state in [:open, :remote_closed] do
if len <= stream.send_window_size do
{:ok, %{stream | send_window_size: stream.send_window_size - len}}
else
{:error, :insufficient_window_size}
end
end
def send_data(%__MODULE__{}, _len) do
{:error, :invalid_state}
end
@spec send_end_of_stream(t(), boolean()) :: {:ok, t()} | {:error, :invalid_state}
def send_end_of_stream(%__MODULE__{state: :open} = stream, true) do
{:ok, %{stream | state: :local_closed}}
end
def send_end_of_stream(%__MODULE__{state: :remote_closed} = stream, true) do
{:ok, %{stream | state: :closed, pid: nil}}
end
def send_end_of_stream(%__MODULE__{}, true) do
{:error, :invalid_state}
end
def send_end_of_stream(%__MODULE__{} = stream, false) do
{:ok, stream}
end
@spec terminate_stream(t(), term()) :: :ok
def terminate_stream(%__MODULE__{pid: pid}, reason) when is_pid(pid) do
# Just kill the process; we will receive a call to stream_terminated once the process actually
# dies, at which point we will transition the struct to the expected final state
Process.exit(pid, reason)
:ok
end
def terminate_stream(%__MODULE__{}, _reason) do
:ok
end
@spec stream_terminated(t(), term()) :: {:ok, t(), Errors.error_code() | nil}
def stream_terminated(%__MODULE__{state: :closed} = stream, :normal) do
{:ok, %{stream | state: :closed, pid: nil}, nil}
end
def stream_terminated(%__MODULE__{} = stream, {:bandit, reason}) do
Logger.warn("Stream #{stream.stream_id} was killed by bandit (#{reason})")
{:ok, %{stream | state: :closed, pid: nil}, nil}
end
def stream_terminated(%__MODULE__{} = stream, :normal) do
Logger.warn("Stream #{stream.stream_id} completed in unexpected state #{stream.state}")
{:ok, %{stream | state: :closed, pid: nil}, Errors.no_error()}
end
def stream_terminated(%__MODULE__{} = stream, reason) do
Logger.error("Task for stream #{stream.stream_id} crashed with #{inspect(reason)}")
{:ok, %{stream | state: :closed, pid: nil}, Errors.internal_error()}
end
end