Packages
bandit
1.1.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/http1/adapter.ex
defmodule Bandit.HTTP1.Adapter do
@moduledoc false
@type read_state :: :unread | :headers_read | :no_body | :body_read
@type write_state :: :unwritten | :sent | :chunking_out
@behaviour Plug.Conn.Adapter
defstruct read_state: :unread,
write_state: :unwritten,
transport_info: nil,
socket: nil,
buffer: <<>>,
body_remaining: nil,
body_encoding: nil,
version: nil,
method: nil,
keepalive: false,
content_encoding: nil,
upgrade: nil,
metrics: %{},
websocket_enabled: false,
opts: []
@typedoc "A struct for backing a Plug.Conn.Adapter"
@type t :: %__MODULE__{
read_state: read_state(),
write_state: write_state(),
transport_info: Bandit.TransportInfo.t(),
socket: ThousandIsland.Socket.t(),
buffer: binary(),
body_remaining: nil | integer(),
body_encoding: nil | binary(),
version: nil | :"HTTP/1.1" | :"HTTP/1.0",
method: Plug.Conn.method() | nil,
keepalive: boolean(),
content_encoding: String.t(),
upgrade: nil | {:websocket, opts :: keyword(), websocket_opts :: keyword()},
metrics: %{},
websocket_enabled: boolean(),
opts: %{
required(:http_1) => Bandit.http_1_options(),
required(:websocket) => Bandit.websocket_options()
}
}
################
# Header Reading
################
def read_request_line(req, request_target \\ nil) do
packet_size = Keyword.get(req.opts.http_1, :max_request_line_length, 10_000)
case :erlang.decode_packet(:http_bin, req.buffer, packet_size: packet_size) do
{:more, _len} ->
with {:ok, chunk} <- read_available(req.socket, _read_timeout = nil) do
read_request_line(%{req | buffer: req.buffer <> chunk}, request_target)
end
{:ok, {:http_request, method, request_target, version}, rest} ->
with {:ok, version} <- get_version(version),
{:ok, request_target} <- resolve_request_target(request_target) do
method = to_string(method)
bytes_read = byte_size(req.buffer) - byte_size(rest)
metrics = Map.update(req.metrics, :req_line_bytes, bytes_read, &(&1 + bytes_read))
req = %{req | buffer: rest, version: version, method: method, metrics: metrics}
{:ok, request_target, req}
end
{:ok, {:http_error, reason}, _rest} ->
{:error, "request line read error: #{inspect(reason)}"}
{:error, :invalid} ->
{:error, :request_uri_too_long}
{:error, reason} ->
{:error, reason}
end
end
def read_headers(req) do
with {:ok, headers, %__MODULE__{} = req} <- do_read_headers(req),
{:ok, body_size} <- Bandit.Headers.get_content_length(headers) do
body_encoding = Bandit.Headers.get_header(headers, "transfer-encoding")
content_encoding =
Bandit.Compression.negotiate_content_encoding(
Bandit.Headers.get_header(headers, "accept-encoding"),
Keyword.get(req.opts.http_1, :compress, true)
)
connection = Bandit.Headers.get_header(headers, "connection")
keepalive = should_keepalive?(req.version, connection)
req = %{req | content_encoding: content_encoding, keepalive: keepalive}
case {body_size, body_encoding} do
{nil, nil} ->
{:ok, headers, %{req | read_state: :no_body}}
{body_size, nil} ->
body_remaining = body_size - byte_size(req.buffer)
{:ok, headers, %{req | read_state: :headers_read, body_remaining: body_remaining}}
{nil, body_encoding} ->
{:ok, headers, %{req | read_state: :headers_read, body_encoding: body_encoding}}
{_content_length, _body_encoding} ->
{:error,
"request cannot contain 'content-length' and 'transfer-encoding' (RFC9112§6.3.3)"}
end
end
end
defp do_read_headers(req, headers \\ []) do
packet_size = Keyword.get(req.opts.http_1, :max_header_length, 10_000)
case :erlang.decode_packet(:httph_bin, req.buffer, packet_size: packet_size) do
{:more, _len} ->
with {:ok, chunk} <- read_available(req.socket, _read_timeout = nil) do
req = %{req | buffer: req.buffer <> chunk}
do_read_headers(req, headers)
end
{:ok, {:http_header, _, header, _, value}, rest} ->
bytes_read = byte_size(req.buffer) - byte_size(rest)
metrics = Map.update(req.metrics, :req_header_bytes, bytes_read, &(&1 + bytes_read))
req = %{req | buffer: rest, metrics: metrics}
headers = [{header |> to_string() |> String.downcase(:ascii), value} | headers]
if length(headers) <= Keyword.get(req.opts.http_1, :max_header_count, 50) do
do_read_headers(req, headers)
else
{:error, :too_many_headers}
end
{:ok, :http_eoh, rest} ->
bytes_read = byte_size(req.buffer) - byte_size(rest)
metrics =
req.metrics
|> Map.update(:req_header_bytes, bytes_read, &(&1 + bytes_read))
|> Map.put(:req_header_end_time, Bandit.Telemetry.monotonic_time())
req = %{req | read_state: :headers_read, buffer: rest, metrics: metrics}
{:ok, headers, req}
{:ok, {:http_error, reason}, _rest} ->
{:error, "header read error: #{inspect(reason)}"}
{:error, :invalid} ->
{:error, :header_too_long}
{:error, reason} ->
{:error, reason}
end
end
# `close` & `keep-alive` always means what they say, otherwise keepalive if we're on HTTP/1.1
defp should_keepalive?(_, "close"), do: false
defp should_keepalive?(_, "keep-alive"), do: true
defp should_keepalive?(:"HTTP/1.1", _), do: true
defp should_keepalive?(_, _), do: false
defp get_version({1, 1}), do: {:ok, :"HTTP/1.1"}
defp get_version({1, 0}), do: {:ok, :"HTTP/1.0"}
defp get_version(other), do: {:error, "invalid HTTP version: #{inspect(other)}"}
# Unwrap different request_targets returned by :erlang.decode_packet/3
defp resolve_request_target({:abs_path, path}), do: {:ok, {nil, nil, nil, path}}
defp resolve_request_target({:absoluteURI, scheme, host, :undefined, path}),
do: {:ok, {to_string(scheme), host, nil, path}}
defp resolve_request_target({:absoluteURI, scheme, host, port, path}),
do: {:ok, {to_string(scheme), host, port, path}}
defp resolve_request_target(:*), do: {:ok, {nil, nil, nil, :*}}
defp resolve_request_target({:scheme, _scheme, _path}),
do: {:error, "schemeURI is not supported"}
defp resolve_request_target(_request_target),
do: {:error, "Unsupported request target (RFC9112§3.2)"}
##############
# Body Reading
##############
@dialyzer {:no_improper_lists, read_req_body: 2}
@impl Plug.Conn.Adapter
@spec read_req_body(t(), keyword()) ::
{:ok, data :: binary(), t()} | {:more, data :: binary(), t()} | {:error, term()}
def read_req_body(%__MODULE__{read_state: :no_body} = req, _opts) do
time = Bandit.Telemetry.monotonic_time()
metrics =
req.metrics
|> Map.put(:req_body_bytes, 0)
|> Map.put(:req_body_start_time, time)
|> Map.put(:req_body_end_time, time)
{:ok, <<>>, %{req | metrics: metrics}}
end
def read_req_body(
%__MODULE__{read_state: :headers_read, body_remaining: body_remaining} = req,
opts
)
when is_number(body_remaining) do
metrics =
Map.put_new_lazy(req.metrics, :req_body_start_time, &Bandit.Telemetry.monotonic_time/0)
with {:ok, to_return, buffer, body_remaining} <-
do_read_content_length_body(req.socket, req.buffer, body_remaining, opts) do
if byte_size(buffer) == 0 && body_remaining == 0 do
metrics =
metrics
|> Map.update(:req_body_bytes, byte_size(to_return), &(&1 + byte_size(to_return)))
|> Map.put(:req_body_end_time, Bandit.Telemetry.monotonic_time())
{:ok, to_return,
%{req | read_state: :body_read, buffer: <<>>, body_remaining: 0, metrics: metrics}}
else
metrics =
metrics
|> Map.update(:req_body_bytes, byte_size(to_return), &(&1 + byte_size(to_return)))
{:more, to_return,
%{req | buffer: buffer, body_remaining: body_remaining, metrics: metrics}}
end
end
end
def read_req_body(%__MODULE__{read_state: :headers_read, body_encoding: "chunked"} = req, opts) do
metrics =
Map.put_new_lazy(req.metrics, :req_body_start_time, &Bandit.Telemetry.monotonic_time/0)
read_size = Keyword.get(opts, :read_length, 1_000_000)
read_timeout = Keyword.get(opts, :read_timeout)
with {:ok, body, buffer} <-
do_read_chunked_body(req.socket, req.buffer, <<>>, read_size, read_timeout) do
body = IO.iodata_to_binary(body)
metrics =
metrics
|> Map.put(:req_body_bytes, byte_size(body))
|> Map.put(:req_body_end_time, Bandit.Telemetry.monotonic_time())
{:ok, body, %{req | read_state: :body_read, buffer: buffer, metrics: metrics}}
end
end
def read_req_body(%__MODULE__{read_state: :headers_read, body_encoding: body_encoding}, _opts)
when not is_nil(body_encoding) do
{:error, :unsupported_transfer_encoding}
end
def read_req_body(%__MODULE__{}, _opts), do: raise(Bandit.BodyAlreadyReadError)
@dialyzer {:no_improper_lists, do_read_content_length_body: 4}
defp do_read_content_length_body(socket, buffer, body_remaining, opts) do
max_desired_bytes = Keyword.get(opts, :length, 8_000_000)
cond do
body_remaining < 0 ->
# We have read more bytes than content-length suggested should have been sent. This is
# veering into request smuggling territory and should never happen with a well behaved
# client. The safest thing to do is just error
{:error, :excess_body_read}
byte_size(buffer) >= max_desired_bytes || body_remaining == 0 ->
# We can satisfy the read request entirely from our buffer
bytes_to_return = min(max_desired_bytes, byte_size(buffer))
<<to_return::binary-size(bytes_to_return), rest::binary>> = buffer
{:ok, to_return, rest, body_remaining}
true ->
# We need to read off the wire
bytes_to_read = min(max_desired_bytes - byte_size(buffer), body_remaining)
read_size = Keyword.get(opts, :read_length, 1_000_000)
read_timeout = Keyword.get(opts, :read_timeout)
with {:ok, iolist} <- read(socket, bytes_to_read, [], read_size, read_timeout) do
to_return = IO.iodata_to_binary([buffer | iolist])
body_remaining = body_remaining - (byte_size(to_return) - byte_size(buffer))
{:ok, to_return, <<>>, body_remaining}
end
end
end
@dialyzer {:no_improper_lists, do_read_chunked_body: 5}
defp do_read_chunked_body(socket, buffer, body, read_size, read_timeout) do
case :binary.split(buffer, "\r\n") do
["0", _] ->
{:ok, IO.iodata_to_binary(body), buffer}
[chunk_size, rest] ->
chunk_size = String.to_integer(chunk_size, 16)
case rest do
<<next_chunk::binary-size(chunk_size), ?\r, ?\n, rest::binary>> ->
do_read_chunked_body(socket, rest, [body, next_chunk], read_size, read_timeout)
_ ->
to_read = chunk_size - byte_size(rest)
if to_read > 0 do
with {:ok, iolist} <- read(socket, to_read, [], read_size, read_timeout) do
buffer = IO.iodata_to_binary([buffer | iolist])
do_read_chunked_body(socket, buffer, body, read_size, read_timeout)
end
else
with {:ok, chunk} <- read_available(socket, read_timeout) do
buffer = buffer <> chunk
do_read_chunked_body(socket, buffer, body, read_size, read_timeout)
end
end
end
_ ->
with {:ok, chunk} <- read_available(socket, read_timeout) do
buffer = buffer <> chunk
do_read_chunked_body(socket, buffer, body, read_size, read_timeout)
end
end
end
##################
# Internal Reading
##################
@compile {:inline, read_available: 2}
@spec read_available(ThousandIsland.Socket.t(), timeout() | nil) ::
{:ok, binary()} | {:error, :closed | :timeout | :inet.posix()}
defp read_available(socket, read_timeout) do
ThousandIsland.Socket.recv(socket, 0, read_timeout)
end
@dialyzer {:no_improper_lists, read: 5}
@spec read(ThousandIsland.Socket.t(), non_neg_integer(), iolist(), non_neg_integer(), timeout()) ::
{:ok, iolist()} | {:error, :closed | :timeout | :inet.posix()}
defp read(socket, to_read, already_read, read_size, read_timeout) do
with {:ok, chunk} <- ThousandIsland.Socket.recv(socket, min(to_read, read_size), read_timeout) do
remaining_bytes = to_read - byte_size(chunk)
if remaining_bytes > 0 do
read(socket, remaining_bytes, [already_read | chunk], read_size, read_timeout)
else
{:ok, [already_read | chunk]}
end
end
end
##################
# Response Sending
##################
@impl Plug.Conn.Adapter
def send_resp(%__MODULE__{write_state: :sent}, _, _, _), do: raise(Plug.Conn.AlreadySentError)
def send_resp(%__MODULE__{write_state: :chunking_out}, _, _, _),
do: raise(Plug.Conn.AlreadySentError)
def send_resp(%__MODULE__{} = req, status, headers, body) do
start_time = Bandit.Telemetry.monotonic_time()
response_content_encoding_header = Bandit.Headers.get_header(headers, "content-encoding")
response_has_strong_etag =
case Bandit.Headers.get_header(headers, "etag") do
nil -> false
"\W" <> _rest -> false
_strong_etag -> true
end
response_indicates_no_transform =
case Bandit.Headers.get_header(headers, "cache-control") do
nil -> false
header -> "no-transform" in Plug.Conn.Utils.list(header)
end
raw_body_bytes = IO.iodata_length(body)
{body, headers, compression_metrics} =
case {body, req.content_encoding, response_content_encoding_header,
response_has_strong_etag, response_indicates_no_transform} do
{body, content_encoding, nil, false, false}
when raw_body_bytes > 0 and not is_nil(content_encoding) ->
metrics = %{
resp_uncompressed_body_bytes: raw_body_bytes,
resp_compression_method: content_encoding
}
deflate_options = Keyword.get(req.opts.http_1, :deflate_options, [])
deflated_body = Bandit.Compression.compress(body, req.content_encoding, deflate_options)
headers = [{"content-encoding", req.content_encoding} | headers]
{deflated_body, headers, metrics}
_ ->
{body, headers, %{}}
end
compress = Keyword.get(req.opts.http_1, :compress, true)
headers = if compress, do: [{"vary", "accept-encoding"} | headers], else: headers
body_bytes = IO.iodata_length(body)
headers = Bandit.Headers.add_content_length(headers, body_bytes, status)
{header_iodata, header_metrics} = response_header(req.version, status, headers)
{body_iodata, body_metrics} =
if send_resp_body?(req, status) do
{body, %{resp_body_bytes: body_bytes}}
else
{[], %{resp_body_bytes: 0}}
end
_ = ThousandIsland.Socket.send(req.socket, [header_iodata | body_iodata])
metrics =
req.metrics
|> Map.merge(compression_metrics)
|> Map.merge(header_metrics)
|> Map.merge(body_metrics)
|> Map.put(:resp_start_time, start_time)
|> Map.put(:resp_end_time, Bandit.Telemetry.monotonic_time())
{:ok, nil, %{req | write_state: :sent, metrics: metrics}}
end
@impl Plug.Conn.Adapter
def send_file(
%__MODULE__{socket: socket, version: version} = req,
status,
headers,
path,
offset,
length
) do
start_time = Bandit.Telemetry.monotonic_time()
%File.Stat{type: :regular, size: size} = File.stat!(path)
length =
cond do
length == :all -> size - offset
is_integer(length) -> length
end
if offset + length <= size do
headers = Bandit.Headers.add_content_length(headers, length, status)
{header_iodata, header_metrics} = response_header(version, status, headers)
_ = ThousandIsland.Socket.send(socket, header_iodata)
body_metrics =
if send_resp_body?(req, status) do
_ = ThousandIsland.Socket.sendfile(socket, path, offset, length)
%{resp_body_bytes: length}
else
%{resp_body_bytes: 0}
end
metrics =
req.metrics
|> Map.merge(header_metrics)
|> Map.merge(body_metrics)
|> Map.put(:resp_start_time, start_time)
|> Map.put(:resp_end_time, Bandit.Telemetry.monotonic_time())
{:ok, nil, %{req | write_state: :sent, metrics: metrics}}
else
{:error,
"Cannot read #{length} bytes starting at #{offset} as #{path} is only #{size} octets in length"}
end
end
@impl Plug.Conn.Adapter
def send_chunked(%__MODULE__{socket: socket, version: version} = req, status, headers) do
start_time = Bandit.Telemetry.monotonic_time()
headers = [{"transfer-encoding", "chunked"} | headers]
{header_iodata, header_metrics} = response_header(version, status, headers)
_ = ThousandIsland.Socket.send(socket, header_iodata)
metrics =
req.metrics
|> Map.merge(header_metrics)
|> Map.put(:resp_start_time, start_time)
|> Map.put(:resp_body_bytes, 0)
if send_resp_body?(req, status) do
{:ok, nil, %{req | write_state: :chunking_out, metrics: metrics}}
else
{:ok, nil, %{req | write_state: :sent, metrics: metrics}}
end
end
@impl Plug.Conn.Adapter
def chunk(%__MODULE__{write_state: :chunking_out, socket: socket}, chunk) do
byte_size = chunk |> IO.iodata_length() |> Integer.to_string(16)
ThousandIsland.Socket.send(socket, [byte_size, "\r\n", chunk, "\r\n"])
end
def chunk(_, _), do: :ok
@impl Plug.Conn.Adapter
def inform(%__MODULE__{version: :"HTTP/1.0"}, _status, _headers), do: {:error, :not_supported}
def inform(%__MODULE__{socket: socket, version: version} = req, status, headers) do
start_time = Bandit.Telemetry.monotonic_time()
{header_iodata, header_metrics} = response_header(version, status, headers)
_ = ThousandIsland.Socket.send(socket, header_iodata)
metrics =
req.metrics
|> Map.merge(header_metrics)
|> Map.put(:resp_start_time, start_time)
|> Map.put(:resp_body_bytes, 0)
{:ok, nil, %{req | metrics: metrics}}
end
defp response_header(nil, status, headers), do: response_header("HTTP/1.0", status, headers)
defp response_header(version, status, headers) do
resp_line = [
to_string(version),
" ",
to_string(status),
" ",
Plug.Conn.Status.reason_phrase(status),
"\r\n"
]
headers =
if is_nil(Bandit.Headers.get_header(headers, "date")) do
[Bandit.Clock.date_header() | headers]
else
headers
end
|> Enum.map(fn {k, v} -> [k, ": ", v, "\r\n"] end)
|> then(&[&1 | ["\r\n"]])
metrics = %{
resp_line_bytes: IO.iodata_length(resp_line),
resp_header_bytes: IO.iodata_length(headers)
}
{[resp_line, headers], metrics}
end
defp send_resp_body?(%{method: "HEAD"}, _status), do: false
defp send_resp_body?(_req, 204), do: false
defp send_resp_body?(_req, 304), do: false
defp send_resp_body?(_req, _status), do: true
@impl Plug.Conn.Adapter
def upgrade(%Bandit.HTTP1.Adapter{websocket_enabled: true} = req, :websocket, opts),
do: {:ok, %{req | upgrade: {:websocket, opts, req.opts.websocket}}}
def upgrade(_req, _upgrade, _opts), do: {:error, :not_supported}
@impl Plug.Conn.Adapter
def push(_req, _path, _headers), do: {:error, :not_supported}
@impl Plug.Conn.Adapter
def get_peer_data(%__MODULE__{transport_info: transport_info}),
do: Bandit.TransportInfo.peer_data(transport_info)
@impl Plug.Conn.Adapter
def get_http_protocol(%__MODULE__{version: version}), do: version
end