Packages
bandit
0.3.3
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 """
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
"""
defstruct stream_id: nil, state: nil, pid: nil
require Integer
require Logger
alias Bandit.HTTP2.{Constants, StreamTask}
@typedoc "An HTTP/2 stream identifier"
@type stream_id :: non_neg_integer()
@typedoc "An HTTP/2 stream state"
@type state :: :idle | :open | :local_closed | :remote_closed | :closed
@typedoc "A single HTTP/2 stream"
@type t :: %__MODULE__{stream_id: stream_id(), state: state(), pid: pid() | nil}
def recv_headers(%__MODULE__{} = stream, headers, peer, plug) do
with :ok <- stream_is_idle(stream),
:ok <- stream_id_is_valid(stream.stream_id),
:ok <- headers_all_lowercase(headers),
:ok <- pseudo_headers_all_request(headers),
:ok <- pseudo_headers_first(headers),
:ok <- no_connection_headers(headers),
:ok <- valid_te_header(headers),
:ok <- exactly_one_instance_of(headers, ":scheme"),
:ok <- exactly_one_instance_of(headers, ":method"),
:ok <- exactly_one_instance_of(headers, ":path"),
:ok <- non_empty_path(headers) do
{:ok, pid} = StreamTask.start_link(self(), stream.stream_id, peer, headers, plug)
{:ok, %{stream | state: :open, pid: pid}}
end
end
defp stream_is_idle(stream) do
if stream.state == :idle do
:ok
else
{:error, {:connection, Constants.protocol_error(), "Received HEADERS when not in idle"}}
end
end
# RFC7540§5.1.1 - client initiated streams must be odd
defp stream_id_is_valid(stream_id) do
if Integer.is_odd(stream_id) do
:ok
else
{:error, {:connection, Constants.protocol_error(), "Received HEADERS with even stream_id"}}
end
end
# RFC7540§8.1.2 - all headers name fields must be lowercsae
defp headers_all_lowercase(headers) do
headers
|> Enum.all?(fn {key, _value} -> String.downcase(key) == key end)
|> if do
:ok
else
{:error, {:stream, Constants.protocol_error(), "Received uppercase header"}}
end
end
# RFC7540§8.1.2.1 - only request pseudo headers may appear
defp pseudo_headers_all_request(headers) do
headers
|> Enum.all?(fn
{":" <> key, _value} -> key in ~w[method scheme authority path]
{_key, _value} -> true
end)
|> if do
:ok
else
{:error, {:stream, Constants.protocol_error(), "Received invalid pseudo header"}}
end
end
# RFC7540§8.1.2.2 - pseudo headers must appear first
defp pseudo_headers_first(headers) 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, Constants.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) 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, Constants.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) do
case List.keyfind(headers, "te", 0) do
nil -> :ok
{_, "trailers"} -> :ok
_ -> {:error, {:stream, Constants.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) do
headers
|> Enum.count(fn {key, _value} -> key == header end)
|> case do
1 -> :ok
_ -> {:error, {:stream, Constants.protocol_error(), "Did not receive 1 #{header} headers"}}
end
end
# RFC7540§8.1.2.3 :path must not be empty
defp non_empty_path(headers) do
case List.keyfind(headers, ":path", 0) do
{_, ""} -> {:error, {:stream, Constants.protocol_error(), "Received empty :path"}}
_ -> :ok
end
end
def recv_data(%__MODULE__{state: state} = stream, data) when state in [:open, :local_closed] do
StreamTask.recv_data(stream.pid, data)
{:ok, stream}
end
def recv_data(%__MODULE__{} = stream, _data) do
{:error, {:connection, Constants.protocol_error(), "Received DATA when in #{stream.state}"}}
end
def recv_rst_stream(%__MODULE__{state: :idle}, _error_code) do
{:error, {:connection, Constants.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
def recv_end_of_stream(%__MODULE__{state: :open} = stream, true) do
StreamTask.recv_end_of_stream(stream.pid)
{:ok, %{stream | state: :remote_closed}}
end
def recv_end_of_stream(%__MODULE__{state: :local_closed} = stream, true) do
StreamTask.recv_end_of_stream(stream.pid)
{:ok, %{stream | state: :closed, pid: nil}}
end
def recv_end_of_stream(%__MODULE__{}, true) do
{:error, {:connection, Constants.protocol_error(), "Received unexpected end_stream"}}
end
def recv_end_of_stream(%__MODULE__{} = stream, false) do
{:ok, stream}
end
def owner?(%__MODULE__{pid: pid}, pid), do: :ok
def owner?(%__MODULE__{}, _pid), do: {:error, :not_owner}
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
def send_data(%__MODULE__{state: state} = stream) when state in [:open, :remote_closed] do
{:ok, stream}
end
def send_data(%__MODULE__{}) do
{:error, :invalid_state}
end
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
def stream_terminated(%__MODULE__{state: :closed} = stream, :normal) do
{:ok, %{stream | state: :closed, pid: nil}, nil}
end
def stream_terminated(%__MODULE__{} = stream, :normal) do
Logger.warn("Stream #{stream.stream_id} completed in unepxected state #{stream.state}")
{:ok, %{stream | state: :closed, pid: nil}, Constants.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}, Constants.internal_error()}
end
end