defmodule HTTP.WebSocket.Frame do @moduledoc false import Bitwise @continuation 0x0 @text 0x1 @binary 0x2 @close 0x8 @ping 0x9 @pong 0xA @default_max_message_size 16 * 1024 * 1024 defstruct buffer: <<>>, fragmented_opcode: nil, fragmented_chunks: [], fragmented_size: 0, max_message_size: @default_max_message_size @type opcode :: :text | :binary | :close | :ping | :pong @type event :: {:message, :text | :binary, binary()} | {:close, non_neg_integer() | nil, binary()} | {:ping, binary()} | {:pong, binary()} @type close_error :: {non_neg_integer(), atom()} @type t :: %__MODULE__{} def new_parser(opts \\ []) do %__MODULE__{max_message_size: Keyword.get(opts, :max_message_size, @default_max_message_size)} end @spec encode(opcode(), binary(), keyword()) :: {:ok, binary()} | {:error, term()} def encode(opcode, payload, opts \\ []) when is_binary(payload) do with {:ok, opcode} <- opcode_number(opcode), :ok <- validate_control_payload(opcode, payload) do mask? = Keyword.get(opts, :client?, true) {:ok, encode_frame(opcode, payload, mask?)} end end @spec close_payload(non_neg_integer() | nil, binary()) :: {:ok, binary()} | {:error, term()} def close_payload(nil, ""), do: {:ok, <<>>} def close_payload(code, reason) when is_integer(code) and is_binary(reason) do cond do code != 1000 and code not in 3000..4999 -> {:error, :invalid_close_code} byte_size(reason) > 123 -> {:error, :close_reason_too_long} not String.valid?(reason) -> {:error, :invalid_close_reason} true -> {:ok, <>} end end def close_payload(_code, _reason), do: {:error, :invalid_close_code} @spec parse(t(), binary()) :: {:ok, t(), [event()]} | {:error, close_error()} def parse(%__MODULE__{} = parser, data) when is_binary(data) do parser |> append(data) |> parse_frames([]) end defp opcode_number(:text), do: {:ok, @text} defp opcode_number(:binary), do: {:ok, @binary} defp opcode_number(:close), do: {:ok, @close} defp opcode_number(:ping), do: {:ok, @ping} defp opcode_number(:pong), do: {:ok, @pong} defp opcode_number(_opcode), do: {:error, :invalid_opcode} defp validate_control_payload(opcode, payload) when opcode in [@close, @ping, @pong] do if byte_size(payload) <= 125, do: :ok, else: {:error, :control_payload_too_large} end defp validate_control_payload(_opcode, _payload), do: :ok defp encode_frame(opcode, payload, true) do mask_key = :crypto.strong_rand_bytes(4) [ <<0x80 ||| opcode>>, encode_length(byte_size(payload), true), mask_key, mask(payload, mask_key) ] |> IO.iodata_to_binary() end defp encode_frame(opcode, payload, false) do [<<0x80 ||| opcode>>, encode_length(byte_size(payload), false), payload] |> IO.iodata_to_binary() end defp encode_length(length, mask?) when length <= 125 do <> end defp encode_length(length, mask?) when length <= 65_535 do <> end defp encode_length(length, mask?) do <> end defp mask_bit(true), do: 0x80 defp mask_bit(false), do: 0x00 defp append(%__MODULE__{buffer: buffer} = parser, data) do %{parser | buffer: buffer <> data} end defp parse_frames(%__MODULE__{buffer: buffer} = parser, events) when byte_size(buffer) < 2 do {:ok, parser, Enum.reverse(events)} end defp parse_frames(%__MODULE__{buffer: buffer} = parser, events) do with {:ok, frame, rest} <- take_frame(buffer), {:ok, parser, new_events} <- handle_frame(%{parser | buffer: rest}, frame) do parse_frames(parser, Enum.reverse(new_events) ++ events) else :more -> {:ok, parser, Enum.reverse(events)} {:error, reason} -> {:error, reason} end end defp take_frame(<>) do fin? = (first &&& 0x80) != 0 rsv = first &&& 0x70 opcode = first &&& 0x0F masked? = (second &&& 0x80) != 0 length_code = second &&& 0x7F with :ok <- validate_header(rsv, opcode, masked?), {:ok, length, payload_rest} <- take_length(length_code, rest), {:ok, payload, rest} <- take_payload(payload_rest, length) do {:ok, %{fin?: fin?, opcode: opcode, payload: payload}, rest} end end defp take_length(length, rest) when length <= 125, do: {:ok, length, rest} defp take_length(126, <>) do if length < 126, do: {:error, {1002, :non_minimal_length}}, else: {:ok, length, rest} end defp take_length(126, _rest), do: :more defp take_length(127, <<0::1, length::63, rest::binary>>) do if length <= 65_535, do: {:error, {1002, :non_minimal_length}}, else: {:ok, length, rest} end defp take_length(127, <<1::1, _length::63, _rest::binary>>), do: {:error, {1002, :invalid_length}} defp take_length(127, _rest), do: :more defp take_payload(rest, length) when byte_size(rest) < length, do: :more defp take_payload(rest, length) do <> = rest {:ok, payload, remaining} end defp validate_header(_rsv, opcode, _masked?) when opcode not in [0, 1, 2, 8, 9, 10], do: {:error, {1002, :unknown_opcode}} defp validate_header(rsv, _opcode, _masked?) when rsv != 0, do: {:error, {1002, :unexpected_rsv}} defp validate_header(_rsv, _opcode, true), do: {:error, {1002, :masked_server_frame}} defp validate_header(_rsv, _opcode, false), do: :ok defp handle_frame(_parser, %{opcode: opcode, fin?: false}) when opcode in [@close, @ping, @pong] do {:error, {1002, :fragmented_control_frame}} end defp handle_frame(_parser, %{opcode: opcode, payload: payload}) when opcode in [@close, @ping, @pong] and byte_size(payload) > 125 do {:error, {1002, :control_payload_too_large}} end defp handle_frame(parser, %{opcode: @text, fin?: true, payload: payload}) do with :ok <- validate_size(parser, byte_size(payload)), :ok <- validate_text(payload) do {:ok, parser, [{:message, :text, payload}]} end end defp handle_frame(parser, %{opcode: @binary, fin?: true, payload: payload}) do with :ok <- validate_size(parser, byte_size(payload)) do {:ok, parser, [{:message, :binary, payload}]} end end defp handle_frame(%__MODULE__{fragmented_opcode: nil} = parser, %{ opcode: opcode, fin?: false, payload: payload }) when opcode in [@text, @binary] do with :ok <- validate_size(parser, byte_size(payload)) do {:ok, %{ parser | fragmented_opcode: opcode, fragmented_chunks: [payload], fragmented_size: byte_size(payload) }, []} end end defp handle_frame(_parser, %{opcode: opcode, fin?: false}) when opcode in [@text, @binary] do {:error, {1002, :fragment_already_started}} end defp handle_frame(%__MODULE__{fragmented_opcode: nil}, %{opcode: @continuation}) do {:error, {1002, :unexpected_continuation}} end defp handle_frame(%__MODULE__{fragmented_opcode: opcode} = parser, %{ opcode: @continuation, fin?: fin?, payload: payload }) do size = parser.fragmented_size + byte_size(payload) with :ok <- validate_size(parser, size) do chunks = [payload | parser.fragmented_chunks] if fin? do message = chunks |> Enum.reverse() |> IO.iodata_to_binary() parser = %{parser | fragmented_opcode: nil, fragmented_chunks: [], fragmented_size: 0} emit_fragmented_message(parser, opcode, message) else {:ok, %{parser | fragmented_chunks: chunks, fragmented_size: size}, []} end end end defp handle_frame(parser, %{opcode: @close, payload: payload}) do with {:ok, code, reason} <- parse_close_payload(payload) do {:ok, parser, [{:close, code, reason}]} end end defp handle_frame(parser, %{opcode: @ping, payload: payload}), do: {:ok, parser, [{:ping, payload}]} defp handle_frame(parser, %{opcode: @pong, payload: payload}), do: {:ok, parser, [{:pong, payload}]} defp emit_fragmented_message(parser, @text, message) do with :ok <- validate_text(message) do {:ok, parser, [{:message, :text, message}]} end end defp emit_fragmented_message(parser, @binary, message) do {:ok, parser, [{:message, :binary, message}]} end defp validate_size(parser, size) do if size <= parser.max_message_size, do: :ok, else: {:error, {1009, :message_too_big}} end defp validate_text(payload) do if String.valid?(payload), do: :ok, else: {:error, {1007, :invalid_utf8}} end defp parse_close_payload(<<>>), do: {:ok, nil, ""} defp parse_close_payload(<<_one_byte>>), do: {:error, {1002, :invalid_close_payload}} defp parse_close_payload(<>) do cond do not valid_received_close_code?(code) -> {:error, {1002, :invalid_close_code}} not String.valid?(reason) -> {:error, {1007, :invalid_close_reason}} true -> {:ok, code, reason} end end defp valid_received_close_code?(code) when code in [1005, 1006, 1015], do: false defp valid_received_close_code?(code) when code < 1000, do: false defp valid_received_close_code?(code) when code in 1000..1014, do: true defp valid_received_close_code?(code) when code in 3000..4999, do: true defp valid_received_close_code?(_code), do: false defp mask(payload, <>) do payload |> :binary.bin_to_list() |> Enum.with_index() |> Enum.map(fn {byte, index} -> bxor(byte, Enum.at([a, b, c, d], rem(index, 4))) end) |> :binary.list_to_bin() end end