Packages
bandit
1.5.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/connection.ex
defmodule Bandit.HTTP2.Connection do
@moduledoc false
# Represents the state of an HTTP/2 connection, in a process-free manner. An instance of this
# struct is maintained as the state of a `Bandit.HTTP2.Handler` process, and it moves an HTTP/2
# connection through its lifecycle by calling functions defined on this module
require Logger
defstruct local_settings: %Bandit.HTTP2.Settings{},
remote_settings: %Bandit.HTTP2.Settings{},
fragment_frame: nil,
send_hpack_state: HPAX.new(4096),
recv_hpack_state: HPAX.new(4096),
send_window_size: 65_535,
recv_window_size: 65_535,
streams: %Bandit.HTTP2.StreamCollection{},
pending_sends: [],
transport_info: nil,
telemetry_span: nil,
plug: nil,
opts: %{}
@typedoc "Encapsulates the state of an HTTP/2 connection"
@type t :: %__MODULE__{
local_settings: Bandit.HTTP2.Settings.t(),
remote_settings: Bandit.HTTP2.Settings.t(),
fragment_frame: Bandit.HTTP2.Frame.Headers.t() | nil,
send_hpack_state: HPAX.Table.t(),
recv_hpack_state: HPAX.Table.t(),
send_window_size: non_neg_integer(),
recv_window_size: non_neg_integer(),
streams: Bandit.HTTP2.StreamCollection.t(),
pending_sends: [{Bandit.HTTP2.Stream.stream_id(), iodata(), boolean(), fun()}],
transport_info: Bandit.TransportInfo.t(),
telemetry_span: ThousandIsland.Telemetry.t(),
plug: Bandit.Pipeline.plug_def(),
opts: %{
required(:http) => Bandit.http_options(),
required(:http_2) => Bandit.http_2_options()
}
}
@spec init(ThousandIsland.Socket.t(), Bandit.Pipeline.plug_def(), map()) :: t()
def init(socket, plug, opts) do
connection = %__MODULE__{
local_settings:
struct!(Bandit.HTTP2.Settings, Keyword.get(opts.http_2, :default_local_settings, [])),
transport_info: Bandit.TransportInfo.init(socket),
telemetry_span: ThousandIsland.Socket.telemetry_span(socket),
plug: plug,
opts: opts
}
# Send SETTINGS frame per RFC9113§3.4
%Bandit.HTTP2.Frame.Settings{ack: false, settings: connection.local_settings}
|> send_frame(socket, connection)
connection
end
#
# Receiving while expecting CONTINUATION frames is a special case (RFC9113§6.10); handle it first
#
@spec handle_frame(Bandit.HTTP2.Frame.frame(), ThousandIsland.Socket.t(), t()) :: t()
def handle_frame(
%Bandit.HTTP2.Frame.Continuation{end_headers: true, stream_id: stream_id} = frame,
socket,
%__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{stream_id: stream_id}} =
connection
) do
header_block = connection.fragment_frame.fragment <> frame.fragment
header_frame = %{connection.fragment_frame | end_headers: true, fragment: header_block}
handle_frame(header_frame, socket, %{connection | fragment_frame: nil})
end
def handle_frame(
%Bandit.HTTP2.Frame.Continuation{end_headers: false, stream_id: stream_id} = frame,
_socket,
%__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{stream_id: stream_id}} =
connection
) do
fragment = connection.fragment_frame.fragment <> frame.fragment
check_oversize_fragment!(fragment, connection)
fragment_frame = %{connection.fragment_frame | fragment: fragment}
%{connection | fragment_frame: fragment_frame}
end
def handle_frame(_frame, _socket, %__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{}}) do
connection_error!("Expected CONTINUATION frame (RFC9113§6.10)")
end
#
# Connection-level receiving
#
def handle_frame(%Bandit.HTTP2.Frame.Settings{ack: true}, _socket, connection), do: connection
def handle_frame(%Bandit.HTTP2.Frame.Settings{ack: false} = frame, socket, connection) do
%Bandit.HTTP2.Frame.Settings{ack: true} |> send_frame(socket, connection)
send_hpack_state = HPAX.resize(connection.send_hpack_state, frame.settings.header_table_size)
delta = frame.settings.initial_window_size - connection.remote_settings.initial_window_size
Bandit.HTTP2.StreamCollection.get_pids(connection.streams)
|> Enum.each(&Bandit.HTTP2.Stream.deliver_send_window_update(&1, delta))
do_pending_sends(socket, %{
connection
| remote_settings: frame.settings,
send_hpack_state: send_hpack_state
})
end
def handle_frame(%Bandit.HTTP2.Frame.Ping{ack: true}, _socket, connection), do: connection
def handle_frame(%Bandit.HTTP2.Frame.Ping{ack: false} = frame, socket, connection) do
%Bandit.HTTP2.Frame.Ping{ack: true, payload: frame.payload} |> send_frame(socket, connection)
connection
end
def handle_frame(%Bandit.HTTP2.Frame.Goaway{}, _socket, connection), do: connection
def handle_frame(%Bandit.HTTP2.Frame.WindowUpdate{stream_id: 0} = frame, socket, connection) do
case Bandit.HTTP2.FlowControl.update_send_window(
connection.send_window_size,
frame.size_increment
) do
{:ok, new_window} -> do_pending_sends(socket, %{connection | send_window_size: new_window})
{:error, error} -> connection_error!(error, Bandit.HTTP2.Errors.flow_control_error())
end
end
#
# Stream-level receiving
#
def handle_frame(%Bandit.HTTP2.Frame.WindowUpdate{} = frame, _socket, connection) do
streams =
with_stream(connection, frame.stream_id, fn stream ->
Bandit.HTTP2.Stream.deliver_send_window_update(stream, frame.size_increment)
end)
%{connection | streams: streams}
end
def handle_frame(%Bandit.HTTP2.Frame.Headers{end_headers: true} = frame, _socket, connection) do
check_oversize_fragment!(frame.fragment, connection)
case HPAX.decode(frame.fragment, connection.recv_hpack_state) do
{:ok, headers, recv_hpack_state} ->
streams =
with_stream(connection, frame.stream_id, fn stream ->
Bandit.HTTP2.Stream.deliver_headers(stream, headers, frame.end_stream)
end)
%{connection | recv_hpack_state: recv_hpack_state, streams: streams}
_ ->
connection_error!("Header decode error", Bandit.HTTP2.Errors.compression_error())
end
end
def handle_frame(%Bandit.HTTP2.Frame.Headers{end_headers: false} = frame, _socket, connection) do
check_oversize_fragment!(frame.fragment, connection)
%{connection | fragment_frame: frame}
end
def handle_frame(%Bandit.HTTP2.Frame.Continuation{}, _socket, _connection) do
connection_error!("Received unexpected CONTINUATION frame (RFC9113§6.10)")
end
def handle_frame(%Bandit.HTTP2.Frame.Data{} = frame, socket, connection) do
streams =
with_stream(connection, frame.stream_id, fn stream ->
Bandit.HTTP2.Stream.deliver_data(stream, frame.data, frame.end_stream)
end)
{recv_window_size, window_increment} =
Bandit.HTTP2.FlowControl.compute_recv_window(
connection.recv_window_size,
byte_size(frame.data)
)
if window_increment > 0 do
%Bandit.HTTP2.Frame.WindowUpdate{stream_id: 0, size_increment: window_increment}
|> send_frame(socket, connection)
end
%{connection | recv_window_size: recv_window_size, streams: streams}
end
def handle_frame(%Bandit.HTTP2.Frame.Priority{}, _socket, connection), do: connection
def handle_frame(%Bandit.HTTP2.Frame.RstStream{} = frame, _socket, connection) do
streams =
with_stream(connection, frame.stream_id, fn stream ->
Bandit.HTTP2.Stream.deliver_rst_stream(stream, frame.error_code)
end)
%{connection | streams: streams}
end
# Catch-all handler for unknown frame types
def handle_frame(%Bandit.HTTP2.Frame.Unknown{} = frame, _socket, connection) do
Logger.warning("Unknown frame (#{inspect(Map.from_struct(frame))})")
connection
end
defp with_stream(connection, stream_id, fun) do
case Bandit.HTTP2.StreamCollection.get_pid(connection.streams, stream_id) do
pid when is_pid(pid) or pid == :closed ->
fun.(pid)
connection.streams
:new ->
if accept_stream?(connection) do
stream =
Bandit.HTTP2.Stream.init(
self(),
stream_id,
connection.remote_settings.initial_window_size,
connection.transport_info
)
case Bandit.HTTP2.StreamProcess.start_link(
stream,
connection.plug,
connection.telemetry_span,
connection.opts
) do
{:ok, pid} ->
streams = Bandit.HTTP2.StreamCollection.insert(connection.streams, stream_id, pid)
with_stream(%{connection | streams: streams}, stream_id, fun)
_ ->
raise "Unable to start stream process"
end
else
connection_error!("Connection count exceeded", Bandit.HTTP2.Errors.refused_stream())
end
:invalid ->
connection_error!("Received invalid stream identifier")
end
end
defp accept_stream?(connection) do
max_requests = Keyword.get(connection.opts.http_2, :max_requests, 0)
max_requests == 0 ||
Bandit.HTTP2.StreamCollection.stream_count(connection.streams) < max_requests
end
defp check_oversize_fragment!(fragment, connection) do
if byte_size(fragment) > Keyword.get(connection.opts.http_2, :max_header_block_size, 50_000),
do: connection_error!("Received overlong headers")
end
# Shared logic to send any pending frames upon adjustment of our send window
defp do_pending_sends(socket, connection) do
connection.pending_sends
|> Enum.reverse()
|> Enum.reduce(connection, fn pending_send, connection ->
connection = connection |> Map.update!(:pending_sends, &List.delete(&1, pending_send))
{stream_id, rest, end_stream, on_unblock} = pending_send
send_data(stream_id, rest, end_stream, on_unblock, socket, connection)
end)
end
#
# Sending logic
#
# All callers of functions below will be from stream processes
#
#
# Stream-level sending
#
@spec send_headers(
Bandit.HTTP2.Stream.stream_id(),
Plug.Conn.headers(),
boolean(),
ThousandIsland.Socket.t(),
t()
) :: t()
def send_headers(stream_id, headers, end_stream, socket, connection) do
with enc_headers <- Enum.map(headers, fn {key, value} -> {:store, key, value} end),
{block, send_hpack_state} <- HPAX.encode(enc_headers, connection.send_hpack_state) do
%Bandit.HTTP2.Frame.Headers{
stream_id: stream_id,
end_stream: end_stream,
fragment: block
}
|> send_frame(socket, connection)
%{connection | send_hpack_state: send_hpack_state}
end
end
@spec send_data(
Bandit.HTTP2.Stream.stream_id(),
iodata(),
boolean(),
fun(),
ThousandIsland.Socket.t(),
t()
) :: t()
def send_data(stream_id, data, end_stream, on_unblock, socket, connection) do
with connection_window_size <- connection.send_window_size,
max_bytes_to_send <- max(connection_window_size, 0),
{data_to_send, bytes_to_send, rest} <- split_data(data, max_bytes_to_send),
connection <- %{connection | send_window_size: connection_window_size - bytes_to_send},
end_stream_to_send <- end_stream && byte_size(rest) == 0 do
if end_stream_to_send || bytes_to_send > 0 do
%Bandit.HTTP2.Frame.Data{
stream_id: stream_id,
end_stream: end_stream_to_send,
data: data_to_send
}
|> send_frame(socket, connection)
end
if byte_size(rest) == 0 do
on_unblock.()
connection
else
pending_sends = [{stream_id, rest, end_stream, on_unblock} | connection.pending_sends]
%{connection | pending_sends: pending_sends}
end
end
end
defp split_data(data, desired_length) do
data_length = IO.iodata_length(data)
if data_length <= desired_length do
{data, data_length, <<>>}
else
<<to_send::binary-size(desired_length), rest::binary>> = IO.iodata_to_binary(data)
{to_send, desired_length, rest}
end
end
@spec send_recv_window_update(
Bandit.HTTP2.Stream.stream_id(),
non_neg_integer(),
ThousandIsland.Socket.t(),
t()
) :: term()
def send_recv_window_update(stream_id, size_increment, socket, connection) do
%Bandit.HTTP2.Frame.WindowUpdate{stream_id: stream_id, size_increment: size_increment}
|> send_frame(socket, connection)
end
@spec send_rst_stream(
Bandit.HTTP2.Stream.stream_id(),
Bandit.HTTP2.Errors.error_code(),
ThousandIsland.Socket.t(),
t()
) :: term()
def send_rst_stream(stream_id, error_code, socket, connection) do
%Bandit.HTTP2.Frame.RstStream{stream_id: stream_id, error_code: error_code}
|> send_frame(socket, connection)
end
@spec stream_terminated(pid(), t()) :: t()
def stream_terminated(pid, connection) do
%{connection | streams: Bandit.HTTP2.StreamCollection.delete(connection.streams, pid)}
end
#
# Helper functions
#
@spec close_connection(Bandit.HTTP2.Errors.error_code(), term(), ThousandIsland.Socket.t(), t()) ::
{:close, t()} | {:error, term(), t()}
def close_connection(error_code, reason, socket, connection) do
last_stream_id = Bandit.HTTP2.StreamCollection.last_stream_id(connection.streams)
%Bandit.HTTP2.Frame.Goaway{last_stream_id: last_stream_id, error_code: error_code}
|> send_frame(socket, connection)
if error_code == Bandit.HTTP2.Errors.no_error(),
do: {:close, connection},
else: {:error, reason, connection}
end
@spec connection_error!(term()) :: no_return()
@spec connection_error!(term(), Bandit.HTTP2.Errors.error_code()) :: no_return()
defp connection_error!(message, error_code \\ Bandit.HTTP2.Errors.protocol_error()) do
raise Bandit.HTTP2.Errors.ConnectionError, message: message, error_code: error_code
end
defp send_frame(frame, socket, connection) do
_ =
ThousandIsland.Socket.send(
socket,
Bandit.HTTP2.Frame.serialize(frame, connection.remote_settings.max_frame_size)
)
:ok
end
end