Packages
bandit
1.6.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/websocket/connection.ex
defmodule Bandit.WebSocket.Connection do
@moduledoc false
# Implementation of a WebSocket lifecycle, implemented using a Socket protocol for communication
alias Bandit.WebSocket.{Frame, PerMessageDeflate, Socket}
defstruct websock: nil,
websock_state: nil,
state: :open,
compress: nil,
opts: [],
fragment_frame: nil,
span: nil,
metrics: %{}
@typedoc "Connection state"
@type state :: :open | :closing | :closed
@typedoc "Encapsulates the state of a WebSocket connection"
@type t :: %__MODULE__{
websock: WebSock.impl(),
websock_state: WebSock.state(),
state: state(),
compress: PerMessageDeflate.t() | nil,
opts: keyword(),
fragment_frame: Frame.Text.t() | Frame.Binary.t() | nil,
span: Bandit.Telemetry.t(),
metrics: map()
}
def init(websock, websock_state, connection_opts, socket) do
compress = Keyword.get(connection_opts, :compress)
connection_telemetry_span_context =
ThousandIsland.Socket.telemetry_span(socket).telemetry_span_context
span =
Bandit.Telemetry.start_span(:websocket, %{compress: compress}, %{
connection_telemetry_span_context: connection_telemetry_span_context,
websock: websock
})
instance = %__MODULE__{
websock: websock,
websock_state: websock_state,
compress: compress,
opts: connection_opts,
span: span
}
websock.init(websock_state) |> handle_continutation(socket, instance)
end
def handle_frame(frame, socket, %{fragment_frame: nil} = connection) do
connection = do_recv_metrics(frame, connection)
case frame do
%Frame.Continuation{} ->
do_error(1002, "Received unexpected continuation frame (RFC6455ยง5.4)", socket, connection)
%Frame.Text{fin: true, compressed: true} = frame ->
do_inflate(frame, socket, connection)
%Frame.Text{fin: true} = frame ->
if !Keyword.get(connection.opts, :validate_text_frames, true) || String.valid?(frame.data) do
connection.websock.handle_in({frame.data, opcode: :text}, connection.websock_state)
|> handle_continutation(socket, connection)
else
do_error(1007, "Received non UTF-8 text frame (RFC6455ยง8.1)", socket, connection)
end
%Frame.Text{fin: false} = frame ->
{:continue, %{connection | fragment_frame: frame}}
%Frame.Binary{fin: true, compressed: true} = frame ->
do_inflate(frame, socket, connection)
%Frame.Binary{fin: true} = frame ->
connection.websock.handle_in({frame.data, opcode: :binary}, connection.websock_state)
|> handle_continutation(socket, connection)
%Frame.Binary{fin: false} = frame ->
{:continue, %{connection | fragment_frame: frame}}
frame ->
handle_control_frame(frame, socket, connection)
end
end
def handle_frame(frame, socket, %{fragment_frame: fragment_frame} = connection)
when not is_nil(fragment_frame) do
connection = do_recv_metrics(frame, connection)
case frame do
%Frame.Continuation{fin: true} = frame ->
data = IO.iodata_to_binary([connection.fragment_frame.data | frame.data])
frame = %{connection.fragment_frame | fin: true, data: data}
handle_frame(frame, socket, %{connection | fragment_frame: nil})
%Frame.Continuation{fin: false} = frame ->
data = [connection.fragment_frame.data | frame.data]
frame = %{connection.fragment_frame | fin: true, data: data}
{:continue, %{connection | fragment_frame: frame}}
%Frame.Text{} ->
do_error(1002, "Received unexpected text frame (RFC6455ยง5.4)", socket, connection)
%Frame.Binary{} ->
do_error(1002, "Received unexpected binary frame (RFC6455ยง5.4)", socket, connection)
frame ->
handle_control_frame(frame, socket, connection)
end
end
defp handle_control_frame(frame, socket, connection) do
case frame do
%Frame.ConnectionClose{} = frame ->
# This is a bit of a subtle case, see RFC6455ยง7.4.1-2
reply_code =
case frame.code do
code when code in 1000..1003 or code in 1007..1011 or code > 2999 -> 1000
_code -> 1002
end
_ = do_stop(reply_code, :remote, socket, connection)
{:close, %{connection | state: :closed}}
%Frame.Ping{} = frame ->
connection =
Socket.send_frame(socket, {:pong, frame.data}, false)
|> do_send_metrics(connection)
if function_exported?(connection.websock, :handle_control, 2) do
connection.websock.handle_control({frame.data, opcode: :ping}, connection.websock_state)
|> handle_continutation(socket, connection)
else
{:continue, connection}
end
%Frame.Pong{} = frame ->
if function_exported?(connection.websock, :handle_control, 2) do
connection.websock.handle_control({frame.data, opcode: :pong}, connection.websock_state)
|> handle_continutation(socket, connection)
else
{:continue, connection}
end
end
end
defp do_recv_metrics(frame, connection) do
metrics =
Bandit.WebSocket.Frame.recv_metrics(frame)
|> Enum.reduce(connection.metrics, fn {key, value}, metrics ->
Map.update(metrics, key, value, &(&1 + value))
end)
%{connection | metrics: metrics}
end
defp do_send_metrics(metrics, connection) do
metrics =
metrics
|> Enum.reduce(connection.metrics, fn {key, value}, metrics ->
Map.update(metrics, key, value, &(&1 + value))
end)
%{connection | metrics: metrics}
end
def handle_close(socket, connection), do: do_error(1006, :closed, socket, connection)
# Some uncertainty if this should be 1000 or 1001 @ https://github.com/mtrudel/bandit/issues/89
def handle_shutdown(socket, connection), do: do_stop(1000, :shutdown, socket, connection)
def handle_error({:deserializing, :max_frame_size_exceeded = reason}, socket, connection),
do: do_error(1009, reason, socket, connection)
def handle_error({:deserializing, reason}, socket, connection),
do: do_error(1002, reason, socket, connection)
def handle_error(reason, socket, connection), do: do_error(1011, reason, socket, connection)
def handle_timeout(socket, connection), do: do_error(1002, :timeout, socket, connection)
def handle_info(msg, socket, connection) do
connection.websock.handle_info(msg, connection.websock_state)
|> handle_continutation(socket, connection)
end
defp handle_continutation(continutation, socket, connection) do
case continutation do
{:ok, websock_state} ->
{:continue, %{connection | websock_state: websock_state}}
{:reply, _status, msg, websock_state} ->
do_deflate(msg, socket, %{connection | websock_state: websock_state})
{:push, msg, websock_state} ->
do_deflate(msg, socket, %{connection | websock_state: websock_state})
{:stop, :normal, websock_state} ->
do_stop(1000, :normal, socket, %{connection | websock_state: websock_state})
{:stop, :normal, code, websock_state} ->
do_stop(code, :normal, socket, %{connection | websock_state: websock_state})
{:stop, :normal, code, msg, websock_state} ->
case do_deflate(msg, socket, %{connection | websock_state: websock_state}) do
{:continue, connection} -> do_stop(code, :normal, socket, connection)
other -> other
end
{:stop, {:shutdown, :restart}, websock_state} ->
do_stop(1012, :normal, socket, %{connection | websock_state: websock_state})
{:stop, reason, websock_state} ->
do_error(1011, reason, socket, %{connection | websock_state: websock_state})
{:stop, reason, code, websock_state} ->
do_error(code, reason, socket, %{connection | websock_state: websock_state})
{:stop, reason, code, msg, websock_state} ->
case do_deflate(msg, socket, %{connection | websock_state: websock_state}) do
{:continue, connection} -> do_error(code, reason, socket, connection)
other -> other
end
end
end
defp do_stop(code, reason, socket, connection) do
if connection.state == :open do
if function_exported?(connection.websock, :terminate, 2) do
connection.websock.terminate(reason, connection.websock_state)
end
if connection.compress, do: PerMessageDeflate.close(connection.compress)
_ = Socket.close(socket, code)
Bandit.Telemetry.stop_span(connection.span, connection.metrics)
end
{:continue, %{connection | state: :closing}}
end
defp do_error(code, reason, socket, connection) do
if connection.state == :open do
if function_exported?(connection.websock, :terminate, 2) do
connection.websock.terminate(maybe_wrap_reason(reason), connection.websock_state)
end
if connection.compress, do: PerMessageDeflate.close(connection.compress)
_ = Socket.close(socket, code)
Bandit.Telemetry.stop_span(connection.span, connection.metrics, %{error: reason})
end
{:error, reason, %{connection | state: :closed}}
end
defp maybe_wrap_reason(:timeout), do: :timeout
defp maybe_wrap_reason(reason), do: {:error, reason}
defp do_deflate(msgs, socket, connection) when is_list(msgs) do
Enum.reduce(msgs, {:continue, connection}, fn
msg, {:continue, connection} -> do_deflate(msg, socket, connection)
_msg, other -> other
end)
end
defp do_deflate({opcode, data} = msg, socket, connection) when opcode in [:text, :binary] do
case PerMessageDeflate.deflate(data, connection.compress) do
{:ok, data, compress} ->
connection =
Socket.send_frame(socket, {opcode, data}, true)
|> do_send_metrics(connection)
{:continue, %{connection | compress: compress}}
{:error, :no_compress} ->
connection =
Socket.send_frame(socket, msg, false)
|> do_send_metrics(connection)
{:continue, connection}
{:error, _reason} ->
do_error(1007, "Deflation error", socket, connection)
end
end
defp do_deflate({opcode, _data} = msg, socket, connection) when opcode in [:ping, :pong] do
connection =
Socket.send_frame(socket, msg, false)
|> do_send_metrics(connection)
{:continue, connection}
end
defp do_inflate(frame, socket, connection) do
case PerMessageDeflate.inflate(frame.data, connection.compress) do
{:ok, data, compress} ->
frame = %{frame | data: data, compressed: false}
connection = %{connection | compress: compress}
handle_frame(frame, socket, connection)
{:error, :no_compress} ->
do_error(1002, "Received unexpected compressed frame (RFC6455ยง5.2)", socket, connection)
{:error, _reason} ->
do_error(1007, "Inflation error", socket, connection)
end
end
end