Packages
bandit
0.4.0
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/http1/adapter.ex
defmodule Bandit.HTTP1.Adapter do
@moduledoc false
@type state :: :new | :headers_read | :no_body | :body_read | :sent | :chunking_out
@behaviour Plug.Conn.Adapter
defstruct state: :new,
socket: nil,
buffer: <<>>,
body_size: nil,
body_encoding: nil,
connection: nil,
version: nil,
keepalive: false
alias ThousandIsland.Socket
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
# credo:disable-for-this-file Credo.Check.Refactor.CondStatements
# credo:disable-for-this-file Credo.Check.Refactor.Nesting
################
# Header Reading
################
def read_headers(req) do
case do_read_headers(req) do
{:ok, headers, method, path, %__MODULE__{version: version, buffer: buffer} = req} ->
body_size = get_header(headers, "content-length")
body_encoding = get_header(headers, "transfer-encoding")
connection = get_header(headers, "connection")
keepalive = should_keepalive?(version, headers)
case {body_size, body_encoding} do
{nil, nil} ->
{:ok, headers, method, path,
%{req | state: :no_body, connection: connection, keepalive: keepalive}}
{body_size, nil} ->
{:ok, headers, method, path,
%{
req
| state: :headers_read,
body_size: String.to_integer(body_size) - byte_size(buffer),
connection: connection,
keepalive: keepalive
}}
{_, body_encoding} ->
{:ok, headers, method, path,
%{
req
| state: :headers_read,
body_encoding: body_encoding,
connection: connection,
keepalive: keepalive
}}
end
{:error, reason} ->
{:error, reason}
end
end
defp do_read_headers(req, type \\ :http, headers \\ [], method \\ nil, path \\ nil)
defp do_read_headers(%__MODULE__{buffer: buffer} = req, type, headers, method, path) do
case :erlang.decode_packet(type, buffer, []) do
{:more, _len} ->
case grow_buffer(req, 0) do
{:ok, req} -> do_read_headers(req, type, headers, method, path)
{:error, reason} -> {:error, reason}
end
{:ok, {:http_request, method, {:abs_path, path}, version}, rest} ->
version =
case version do
{1, 1} -> :"HTTP/1.1"
{1, 0} -> :"HTTP/1.0"
end
do_read_headers(%{req | buffer: rest, version: version}, :httph, headers, method, path)
{:ok, {:http_header, _, header, _, value}, rest} ->
do_read_headers(
%{req | buffer: rest},
:httph,
[{header |> to_string() |> String.downcase(), to_string(value)} | headers],
to_string(method),
to_string(path)
)
{:ok, :http_eoh, rest} ->
{:ok, headers, method, path, %{req | state: :headers_read, buffer: rest}}
{:ok, {:http_error, _reason}, _rest} ->
{:error, :invalid_request}
{:error, reason} ->
{:error, reason}
end
end
defp should_keepalive?(version, headers) do
cond do
get_header(headers, "connection") |> is_nil -> version == :"HTTP/1.1"
get_header(headers, "connection") |> String.match?(~r/^keep-alive$/i) -> true
get_header(headers, "connection") |> String.match?(~r/^close$/i) -> false
version == :"HTTP/1.1" -> true
true -> false
end
end
defp get_header(headers, header, default \\ nil) do
case List.keyfind(headers, header, 0) do
{_, value} -> value
nil -> default
end
end
##############
# Body Reading
##############
@impl Plug.Conn.Adapter
def read_req_body(%__MODULE__{state: :no_body} = req, _opts), do: {:ok, nil, req}
def read_req_body(%__MODULE__{state: :headers_read, buffer: buffer, body_size: 0} = req, _opts) do
{:ok, buffer, %{req | state: :body_read, buffer: <<>>, body_size: 0}}
end
def read_req_body(%__MODULE__{state: :headers_read, body_size: body_size} = req, opts)
when is_number(body_size) do
to_read = min(body_size, Keyword.get(opts, :length, 8_000_000))
case grow_buffer(req, to_read, opts) do
{:ok, %__MODULE__{buffer: buffer} = req} ->
remaining_bytes = body_size - byte_size(buffer)
if remaining_bytes > 0 do
{:more, buffer, %{req | buffer: <<>>, body_size: remaining_bytes}}
else
{:ok, buffer, %{req | state: :body_read, buffer: <<>>, body_size: 0}}
end
{:error, reason} ->
{:error, reason}
end
end
def read_req_body(%__MODULE__{state: :headers_read, body_encoding: "chunked"} = req, opts) do
case do_read_chunk(req, <<>>, opts) do
{:ok, body, req} -> {:ok, IO.iodata_to_binary(body), req}
other -> other
end
end
def read_req_body(%__MODULE__{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)
defp do_read_chunk(%__MODULE__{buffer: buffer} = req, body, opts) do
case :binary.match(buffer, "\r\n") do
{offset, _} ->
<<chunk_size::binary-size(offset), ?\r, ?\n, rest::binary>> = buffer
case String.to_integer(chunk_size, 16) do
0 ->
{:ok, body, req}
chunk_size ->
case rest do
<<next_chunk::binary-size(chunk_size), ?\r, ?\n, rest::binary>> ->
do_read_chunk(%{req | buffer: rest}, [body, next_chunk], opts)
_ ->
case grow_buffer(req, chunk_size - byte_size(rest), opts) do
{:ok, req} -> do_read_chunk(req, body, opts)
{:error, reason} -> {:error, reason}
end
end
end
:nomatch ->
case grow_buffer(req, 0, opts) do
{:ok, req} -> do_read_chunk(req, body, opts)
{:error, reason} -> {:error, reason}
end
end
end
##################
# Internal Reading
##################
defp grow_buffer(%__MODULE__{socket: socket, buffer: buffer} = req, to_read, opts \\ []) do
read_size = min(to_read, Keyword.get(opts, :read_length, 1_000_000))
read_timeout = Keyword.get(opts, :read_timeout, 15_000)
case Socket.recv(socket, read_size, read_timeout) do
{:ok, chunk} ->
remaining_bytes = to_read - byte_size(chunk)
if remaining_bytes > 0 do
grow_buffer(%{req | buffer: buffer <> chunk}, remaining_bytes, opts)
else
{:ok, %{req | buffer: buffer <> chunk}}
end
{:error, reason} ->
{:error, reason}
end
end
##################
# Response Sending
##################
@impl Plug.Conn.Adapter
def send_resp(%__MODULE__{state: :sent}, _, _, _), do: raise(Plug.Conn.AlreadySentError)
def send_resp(%__MODULE__{state: :chunking_out}, _, _, _), do: raise(Plug.Conn.AlreadySentError)
def send_resp(%__MODULE__{socket: socket, version: version} = req, status, headers, response) do
headers =
response
|> byte_size()
|> case do
0 -> headers
content_length -> [{"content-length", content_length |> to_string()} | headers]
end
header_io_data = response_header(version, status, headers)
Socket.send(socket, [header_io_data, response])
{:ok, nil, %{req | state: :sent}}
end
@impl Plug.Conn.Adapter
def send_file(
%__MODULE__{socket: socket, version: version} = req,
status,
headers,
path,
offset,
length
) do
%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 = [{"content-length", length |> to_string()} | headers]
Socket.send(socket, response_header(version, status, headers))
Socket.sendfile(socket, path, offset, length)
{:ok, nil, %{req | state: :sent}}
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
headers = [{"transfer-encoding", "chunked"} | headers]
Socket.send(socket, response_header(version, status, headers))
{:ok, nil, %{req | state: :chunking_out}}
end
@impl Plug.Conn.Adapter
def chunk(%__MODULE__{socket: socket}, chunk) do
byte_size = chunk |> byte_size() |> Integer.to_string(16)
Socket.send(socket, [byte_size, "\r\n", chunk, "\r\n"])
:ok
end
defp response_header(nil, status, headers), do: response_header("HTTP/1.0", status, headers)
defp response_header(version, status, headers) do
[
to_string(version),
" ",
to_string(status),
"\r\n",
Enum.map(headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
"\r\n"
]
end
@impl Plug.Conn.Adapter
def inform(_req, _status, _headers), 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__{socket: socket}), do: Socket.peer_info(socket)
def get_local_data(%__MODULE__{socket: socket}), do: Socket.local_info(socket)
def secure?(%__MODULE__{socket: socket}), do: Socket.secure?(socket)
@impl Plug.Conn.Adapter
def get_http_protocol(%__MODULE__{version: version}), do: version
def keepalive?(%__MODULE__{keepalive: keepalive}), do: keepalive
end