Packages
bandit
0.6.4
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
test/bandit/websocket/protocol_test.exs
defmodule WebSocketProtocolTest do
use ExUnit.Case, async: true
use ServerHelpers
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
setup :http_server
def call(conn, _opts) do
conn = Plug.Conn.fetch_query_params(conn)
case Bandit.WebSocket.Handshake.valid_upgrade?(conn) do
true ->
websock = conn.query_params["websock"] |> String.to_atom()
compress = conn.query_params["compress"]
Plug.Conn.upgrade_adapter(conn, :websocket, {websock, conn.params, compress: compress})
false ->
Plug.Conn.send_resp(conn, 204, <<>>)
end
end
# These websocks are used throughout these tests, so declare them top-level
defmodule EchoWebSock do
use NoopWebSock
def handle_in({data, opcode: opcode}, state), do: {:push, {opcode, data}, state}
def handle_control({data, opcode: opcode}, state), do: {:push, {opcode, data}, state}
end
defmodule TerminateWebSock do
use NoopWebSock
def terminate(reason, _state), do: WebSocketProtocolTest.send(reason)
end
setup do
Process.register(self(), __MODULE__)
:ok
end
def send(msg), do: send(__MODULE__, msg)
describe "packet-level frame splitting / merging" do
test "it should handle cases where the frames arrives in small chunks", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
# Send text frame one byte at a time
(<<8::4, 1::4, 1::1, 2::7, 0::32>> <> "OK")
|> Stream.unfold(fn
<<>> -> nil
<<byte::binary-size(1), rest::binary>> -> {byte, rest}
end)
|> Enum.each(fn byte -> :gen_tcp.send(client, byte) end)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
end
test "it should handle cases where a full and partial frame arrive in the same packet",
context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
# Send one and a half frames
:gen_tcp.send(client, <<8::4, 1::4, 1::1, 2::7, 0::32>> <> "OK" <> <<8::4, 1::4>>)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
# Now send the rest of the second frame
:gen_tcp.send(client, <<1::1, 2::7, 0::32>> <> "OK")
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
end
test "it should handle cases where multiple frames arrive in the same packet", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
# Send two text frames at once one byte at a time
:gen_tcp.send(
client,
<<8::4, 1::4, 1::1, 2::7, 0::32>> <> "OK" <> <<8::4, 1::4, 1::1, 2::7, 0::32>> <> "OK"
)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
end
end
describe "s/m/l frame sizes" do
test "small (7 bit) frames are received properly", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
SimpleWebSocketClient.send_text_frame(client, "OK")
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "OK"}
end
test "mid-sized (16 bit) frames are received properly", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
payload = String.duplicate("0123456789", 1_000)
SimpleWebSocketClient.send_text_frame(client, payload)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, payload}
end
test "large-sized (64 bit) frames are received properly", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
payload = String.duplicate("0123456789", 100_000)
SimpleWebSocketClient.send_text_frame(client, payload)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, payload}
end
end
describe "frame fragmentation" do
test "handle_in is called once when fragmented text frames are sent", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
payload = String.duplicate("0123456789", 1_000)
SimpleWebSocketClient.send_text_frame(client, payload, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, payload, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, payload)
expected_payload = String.duplicate(payload, 3)
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, expected_payload}
end
test "handle_in is called once when fragmented binary frames are sent", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
payload = String.duplicate("0123456789", 1_000)
SimpleWebSocketClient.send_binary_frame(client, payload, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, payload, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, payload)
expected_payload = String.duplicate(payload, 3)
assert SimpleWebSocketClient.recv_binary_frame(client) == {:ok, expected_payload}
end
end
describe "compressed frames" do
test "correctly decompresses text frames and sends compressed frames back", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock, [], true)
deflated_payload = <<74, 76, 28, 5, 163, 96, 20, 12, 119, 0, 0>>
SimpleWebSocketClient.send_text_frame(client, deflated_payload, 0xC)
assert SimpleWebSocketClient.recv_deflated_text_frame(client) == {:ok, deflated_payload}
end
test "correctly decompresses binary frames and sends compressed frames back", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock, [], true)
deflated_payload = <<74, 76, 28, 5, 163, 96, 20, 12, 119, 0, 0>>
SimpleWebSocketClient.send_binary_frame(client, deflated_payload, 0xC)
assert SimpleWebSocketClient.recv_deflated_binary_frame(client) == {:ok, deflated_payload}
end
test "correctly decompresses fragmented text frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock, [], true)
deflated_payload = <<74, 76, 28, 5>>
deflated_payload_continuation = <<163, 96, 20, 12>>
deflated_payload_continuation_2 = <<119, 0, 0>>
SimpleWebSocketClient.send_text_frame(client, deflated_payload, 0x4)
SimpleWebSocketClient.send_continuation_frame(client, deflated_payload_continuation, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, deflated_payload_continuation_2)
deflated_payload = <<74, 76, 28, 5, 163, 96, 20, 12, 119, 0, 0>>
assert SimpleWebSocketClient.recv_deflated_text_frame(client) == {:ok, deflated_payload}
end
test "correctly decompresses fragmented binary frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock, [], true)
deflated_payload = <<74, 76, 28, 5>>
deflated_payload_continuation = <<163, 96, 20, 12>>
deflated_payload_continuation_2 = <<119, 0, 0>>
SimpleWebSocketClient.send_binary_frame(client, deflated_payload, 0x4)
SimpleWebSocketClient.send_continuation_frame(client, deflated_payload_continuation, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, deflated_payload_continuation_2)
deflated_payload = <<74, 76, 28, 5, 163, 96, 20, 12, 119, 0, 0>>
assert SimpleWebSocketClient.recv_deflated_binary_frame(client) == {:ok, deflated_payload}
end
test "does not compress ping or pong frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock, [], true)
SimpleWebSocketClient.send_ping_frame(client, "OK")
assert SimpleWebSocketClient.recv_pong_frame(client) == {:ok, "OK"}
assert SimpleWebSocketClient.recv_ping_frame(client) == {:ok, "OK"}
end
end
describe "ping frames" do
test "send a pong per RFC6455§5.5.2", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
SimpleWebSocketClient.send_ping_frame(client, "OK")
assert SimpleWebSocketClient.recv_pong_frame(client) == {:ok, "OK"}
assert SimpleWebSocketClient.recv_ping_frame(client) == {:ok, "OK"}
end
test "are processed when interleaved with continuation frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
SimpleWebSocketClient.send_text_frame(client, "AB", 0x0)
SimpleWebSocketClient.send_ping_frame(client, "OK")
assert SimpleWebSocketClient.recv_pong_frame(client) == {:ok, "OK"}
assert SimpleWebSocketClient.recv_ping_frame(client) == {:ok, "OK"}
SimpleWebSocketClient.send_continuation_frame(client, "CD")
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "ABCD"}
end
end
describe "pong frames" do
test "are processed when interleaved with continuation frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, EchoWebSock)
SimpleWebSocketClient.send_text_frame(client, "AB", 0x0)
SimpleWebSocketClient.send_pong_frame(client, "OK")
assert SimpleWebSocketClient.recv_pong_frame(client) == {:ok, "OK"}
SimpleWebSocketClient.send_continuation_frame(client, "CD")
assert SimpleWebSocketClient.recv_text_frame(client) == {:ok, "ABCD"}
end
end
describe "server-side connection close" do
defmodule ServerSideCloseWebSock do
use NoopWebSock
def handle_in({"normal", opcode: :text}, state), do: {:stop, :normal, state}
def handle_in(_data, state), do: {:push, {:text, :erlang.pid_to_list(self())}, state}
end
test "server does a proper shutdown handshake when closing a connection", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, ServerSideCloseWebSock)
# Find out the server process pid
SimpleWebSocketClient.send_text_frame(client, "whoami")
{:ok, pid} = SimpleWebSocketClient.recv_text_frame(client)
pid = pid |> String.to_charlist() |> :erlang.list_to_pid()
# Get the websock to tell bandit to shut down
SimpleWebSocketClient.send_text_frame(client, "normal")
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1000::16>>}
# Wait a bit and validate that the server is still very much alive
Process.sleep(100)
assert Process.alive?(pid)
# Now send our half of the handshake and verify that the server has shut down
SimpleWebSocketClient.send_connection_close_frame(client, 1000)
Process.sleep(100)
refute Process.alive?(pid)
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
end
describe "client-side connection close" do
defmodule ClientSideCloseWebSock do
use NoopWebSock
def handle_in(_data, state), do: {:push, {:text, :erlang.pid_to_list(self())}, state}
end
test "returns a corresponding connection close frame", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, ClientSideCloseWebSock)
# Find out the server process pid
SimpleWebSocketClient.send_text_frame(client, "whoami")
{:ok, pid} = SimpleWebSocketClient.recv_text_frame(client)
pid = pid |> String.to_charlist() |> :erlang.list_to_pid()
# Close the connection from the client
SimpleWebSocketClient.send_connection_close_frame(client, 1000)
# Now ensure that we see the server's half of the shutdown handshake
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1000::16>>}
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
# Wait a bit and validate that the server is closed
Process.sleep(100)
refute Process.alive?(pid)
end
test "client closes are are processed when interleaved with continuation frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, ClientSideCloseWebSock)
# Find out the server process pid
SimpleWebSocketClient.send_text_frame(client, "whoami")
{:ok, pid} = SimpleWebSocketClient.recv_text_frame(client)
pid = pid |> String.to_charlist() |> :erlang.list_to_pid()
# Note that we don't expect this to send back a pid since it won't make it to the WebSock
SimpleWebSocketClient.send_text_frame(client, "whoami", 0x0)
# Close the connection from the client
SimpleWebSocketClient.send_connection_close_frame(client, 1000)
# Now ensure that we see the server's half of the shutdown handshake
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1000::16>>}
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
# Wait a bit and validate that the server is closed
Process.sleep(100)
refute Process.alive?(pid)
end
end
describe "error handling" do
@tag capture_log: true
test "server sends a 1002 on an unexpected continuation frame", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
# This is not allowed by RFC6455§5.4
SimpleWebSocketClient.send_continuation_frame(client, <<1, 2, 3>>)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received unexpected continuation frame (RFC6455§5.4)"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1002 on a text frame during continuation", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
SimpleWebSocketClient.send_text_frame(client, <<1, 2, 3>>, 0x0)
# This is not allowed by RFC6455§5.4
SimpleWebSocketClient.send_text_frame(client, <<1, 2, 3>>)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received unexpected text frame (RFC6455§5.4)"}
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1002 on a binary frame during continuation", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
SimpleWebSocketClient.send_binary_frame(client, <<1, 2, 3>>, 0x0)
# This is not allowed by RFC6455§5.4
SimpleWebSocketClient.send_binary_frame(client, <<1, 2, 3>>)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received unexpected binary frame (RFC6455§5.4)"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1002 on a compressed frame when deflate not negotiated", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
deflated_payload = <<74, 76, 28, 5, 163, 96, 20, 12, 119, 0, 0>>
SimpleWebSocketClient.send_text_frame(client, deflated_payload, 0xC)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received unexpected compressed frame (RFC6455§5.2)"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1007 on a malformed compressed frame", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock, [], true)
deflated_payload = <<1, 2, 3>>
SimpleWebSocketClient.send_text_frame(client, deflated_payload, 0xC)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Inflation error"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1007::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1007 on a non UTF-8 text frame", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
SimpleWebSocketClient.send_text_frame(client, <<0xE2::8, 0x82::8, 0x28::8>>)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received non UTF-8 text frame (RFC6455§8.1)"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1007::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1007 on fragmented non UTF-8 text frame", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
SimpleWebSocketClient.send_text_frame(client, <<0xE2::8>>, 0x0)
SimpleWebSocketClient.send_continuation_frame(client, <<0x82::8, 0x28::8>>)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive {:error, "Received non UTF-8 text frame (RFC6455§8.1)"}
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1007::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
end
describe "timeout conditions" do
@tag capture_log: true
test "server sends a 1002 if no frames sent at all", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive :timeout, 1500
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
@tag capture_log: true
test "server sends a 1002 on timeout between frames", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TerminateWebSock)
SimpleWebSocketClient.send_text_frame(client, "OK")
# Get the error that terminate saw, to ensure we're closing for the expected reason
assert_receive :timeout, 1500
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1002::16>>}
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
defmodule TimeoutCloseWebSock do
use NoopWebSock
def handle_in({"normal", opcode: :text}, state), do: {:stop, :normal, state}
def handle_in(_data, state), do: {:push, {:text, :erlang.pid_to_list(self())}, state}
end
@tag capture_log: true
test "server times out waiting for client connection close", context do
client = SimpleWebSocketClient.tcp_client(context)
SimpleWebSocketClient.http1_handshake(client, TimeoutCloseWebSock)
# Find out the server process pid
SimpleWebSocketClient.send_text_frame(client, "whoami")
{:ok, pid} = SimpleWebSocketClient.recv_text_frame(client)
pid = pid |> String.to_charlist() |> :erlang.list_to_pid()
# Get the websock to tell bandit to shut down
SimpleWebSocketClient.send_text_frame(client, "normal")
# Validate that the server has started the shutdown handshake from RFC6455§7.1.2
assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1000::16>>}
# Wait a bit and validate that the server is still very much alive
Process.sleep(100)
assert Process.alive?(pid)
# Now wait for the server to timeout
Process.sleep(1500)
# Verify that the server has shut down
refute Process.alive?(pid)
# Verify that the server didn't send any extraneous frames
assert SimpleWebSocketClient.connection_closed_for_reading?(client)
end
end
end