From b273fb54f9f450b643d03c6e583a5b2597b4ff3b Mon Sep 17 00:00:00 2001 From: Justin Baker Date: Fri, 31 Aug 2018 13:53:29 -0500 Subject: [PATCH] Run Code Formatter --- .formatter.exs | 3 + lib/websockex.ex | 457 +++++++++++++++++++++---------- lib/websockex/application.ex | 2 +- lib/websockex/conn.ex | 168 +++++++----- lib/websockex/errors.ex | 52 ++-- lib/websockex/frame.ex | 229 +++++++++++----- lib/websockex/utils.ex | 99 ++++--- mix.exs | 43 +-- test/support/test_server.ex | 75 +++-- test/websockex/conn_test.exs | 87 +++--- test/websockex/frame_test.exs | 423 +++++++++++++++++----------- test/websockex_nonasync_test.exs | 6 +- test/websockex_test.exs | 394 ++++++++++++++++---------- 13 files changed, 1299 insertions(+), 739 deletions(-) create mode 100644 .formatter.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..2bed17c --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lib/websockex.ex b/lib/websockex.ex index 17cd470..04b7127 100644 --- a/lib/websockex.ex +++ b/lib/websockex.ex @@ -68,11 +68,13 @@ defmodule WebSockex do These options can also be set after the process is running using the functions in the Erlang `:sys` module. """ - @type debug_opts :: [:trace - | :log - | {:log, log_depth :: pos_integer} - | :statistics - | {:log_to_file, Path.t}] + @type debug_opts :: [ + :trace + | :log + | {:log, log_depth :: pos_integer} + | :statistics + | {:log_to_file, Path.t()} + ] @type options :: [option] @@ -92,11 +94,12 @@ defmodule WebSockex do Other possible option values include: `t:WebSockex.Conn.connection_option/0` """ - @type option :: WebSockex.Conn.connection_option - | {:async, boolean} - | {:debug, debug_opts} - | {:name, atom} - | {:handle_initial_conn_failure, boolean} + @type option :: + WebSockex.Conn.connection_option() + | {:async, boolean} + | {:debug, debug_opts} + | {:name, atom} + | {:handle_initial_conn_failure, boolean} @typedoc """ The reason a connection was closed. @@ -106,18 +109,20 @@ defmodule WebSockex do If the peer closes the connection abruptly without a close frame then the close reason is `{:remote, :closed}`. """ - @type close_reason :: {:remote | :local, :normal} - | {:remote | :local, close_code, message :: binary} - | {:remote, :closed} - | {:error, term} + @type close_reason :: + {:remote | :local, :normal} + | {:remote | :local, close_code, message :: binary} + | {:remote, :closed} + | {:error, term} @typedoc """ The error returned when a connection fails to be established. """ - @type close_error :: %WebSockex.RequestError{} - | %WebSockex.ConnError{} - | %WebSockex.InvalidFrameError{} - | %WebSockex.FrameEncodeError{} + @type close_error :: + %WebSockex.RequestError{} + | %WebSockex.ConnError{} + | %WebSockex.InvalidFrameError{} + | %WebSockex.FrameEncodeError{} @typedoc """ A map that contains information about the failure to connect. @@ -125,17 +130,18 @@ defmodule WebSockex do This map contains the error, attempt number, and the `t:WebSockex.Conn.t/0` that was used to attempt the connection. """ - @type connection_status_map :: %{reason: close_reason | close_error, - attempt_number: integer, - conn: WebSockex.Conn.t} + @type connection_status_map :: %{ + reason: close_reason | close_error, + attempt_number: integer, + conn: WebSockex.Conn.t() + } @doc """ Invoked after a connection is established. This is invoked after both the initial connection and a reconnect. """ - @callback handle_connect(conn :: WebSockex.Conn.t, state :: term) :: - {:ok, new_state :: term} + @callback handle_connect(conn :: WebSockex.Conn.t(), state :: term) :: {:ok, new_state :: term} @doc """ Invoked on the reception of a frame on the socket. @@ -144,28 +150,31 @@ defmodule WebSockex do then the frame will have `nil` as the payload. e.g. `{:ping, nil}` """ @callback handle_frame(frame, state :: term) :: - {:ok, new_state} - | {:reply, frame, new_state} - | {:close, new_state} - | {:close, close_frame, new_state} when new_state: term + {:ok, new_state} + | {:reply, frame, new_state} + | {:close, new_state} + | {:close, close_frame, new_state} + when new_state: term @doc """ Invoked to handle asynchronous `cast/2` messages. """ @callback handle_cast(msg :: term, state :: term) :: - {:ok, new_state} - | {:reply, frame, new_state} - | {:close, new_state} - | {:close, close_frame, new_state} when new_state: term + {:ok, new_state} + | {:reply, frame, new_state} + | {:close, new_state} + | {:close, close_frame, new_state} + when new_state: term @doc """ Invoked to handle all other non-WebSocket messages. """ @callback handle_info(msg :: term, state :: term) :: - {:ok, new_state} - | {:reply, frame, new_state} - | {:close, new_state} - | {:close, close_frame, new_state} when new_state: term + {:ok, new_state} + | {:reply, frame, new_state} + | {:close, new_state} + | {:close, close_frame, new_state} + when new_state: term @doc """ Invoked when the WebSocket disconnects from the server. @@ -189,27 +198,30 @@ defmodule WebSockex do data in `conn`. `conn` is expected to be a `t:WebSockex.Conn.t/0`. """ @callback handle_disconnect(connection_status_map, state :: term) :: - {:ok, new_state} - | {:reconnect, new_state} - | {:reconnect, new_conn :: WebSockex.Conn.t, new_state} when new_state: term + {:ok, new_state} + | {:reconnect, new_state} + | {:reconnect, new_conn :: WebSockex.Conn.t(), new_state} + when new_state: term @doc """ Invoked when the Websocket receives a ping frame """ @callback handle_ping(ping_frame :: :ping | {:ping, binary}, state :: term) :: - {:ok, new_state} - | {:reply, frame, new_state} - | {:close, new_state} - | {:close, close_frame, new_state} when new_state: term + {:ok, new_state} + | {:reply, frame, new_state} + | {:close, new_state} + | {:close, close_frame, new_state} + when new_state: term @doc """ Invoked when the Websocket receives a pong frame. """ @callback handle_pong(pong_frame :: :pong | {:pong, binary}, state :: term) :: - {:ok, new_state} - | {:reply, frame, new_state} - | {:close, new_state} - | {:close, close_frame, new_state} when new_state: term + {:ok, new_state} + | {:reply, frame, new_state} + | {:close, new_state} + | {:close, close_frame, new_state} + when new_state: term @doc """ Invoked when the process is terminating. @@ -219,10 +231,9 @@ defmodule WebSockex do @doc """ Invoked when a new version the module is loaded during runtime. """ - @callback code_change(old_vsn :: term | {:down, term}, - state :: term, extra :: term) :: - {:ok, new_state :: term} - | {:error, reason :: term} + @callback code_change(old_vsn :: term | {:down, term}, state :: term, extra :: term) :: + {:ok, new_state :: term} + | {:error, reason :: term} @doc """ Invoked to to retrieve a formatted status of the state in a WebSockex process. @@ -233,7 +244,7 @@ defmodule WebSockex do The second argument is a two-element list with the order of `[pdict, state]`. """ @callback format_status(:normal, [process_dictionary | state]) :: status :: term - when process_dictionary: [{key :: term, val :: term}], state: term + when process_dictionary: [{key :: term, val :: term}], state: term @optional_callbacks format_status: 2 @@ -270,18 +281,18 @@ defmodule WebSockex do @doc false def handle_frame(frame, _state) do - raise "No handle_frame/2 clause in #{__MODULE__} provided for #{inspect frame}" + raise "No handle_frame/2 clause in #{__MODULE__} provided for #{inspect(frame)}" end @doc false def handle_cast(message, _state) do - raise "No handle_cast/2 clause in #{__MODULE__} provided for #{inspect message}" + raise "No handle_cast/2 clause in #{__MODULE__} provided for #{inspect(message)}" end @doc false def handle_info(message, state) do require Logger - Logger.error "No handle_info/2 clause in #{__MODULE__} provided for #{inspect message}" + Logger.error("No handle_info/2 clause in #{__MODULE__} provided for #{inspect(message)}") {:ok, state} end @@ -294,6 +305,7 @@ defmodule WebSockex do def handle_ping(:ping, state) do {:reply, :pong, state} end + def handle_ping({:ping, msg}, state) do {:reply, {:pong, msg}, state} end @@ -308,8 +320,15 @@ defmodule WebSockex do @doc false def code_change(_old_vsn, state, _extra), do: {:ok, state} - defoverridable [handle_connect: 2, handle_frame: 2, handle_cast: 2, handle_info: 2, handle_ping: 2, - handle_pong: 2, handle_disconnect: 2, terminate: 2, code_change: 3] + defoverridable handle_connect: 2, + handle_frame: 2, + handle_cast: 2, + handle_info: 2, + handle_ping: 2, + handle_pong: 2, + handle_disconnect: 2, + terminate: 2, + code_change: 3 end end @@ -320,17 +339,20 @@ defmodule WebSockex do See `start_link/4` for more information. """ - @spec start(url :: String.t | WebSockex.Conn.t, module, term, options) :: - {:ok, pid} | {:error, term} + @spec start(url :: String.t() | WebSockex.Conn.t(), module, term, options) :: + {:ok, pid} | {:error, term} def start(conn_info, module, state, opts \\ []) + def start(%WebSockex.Conn{} = conn, module, state, opts) do Utils.spawn(:no_link, conn, module, state, opts) end + def start(url, module, state, opts) do case WebSockex.Conn.parse_url(url) do {:ok, uri} -> conn = WebSockex.Conn.new(uri, opts) start(conn, module, state, opts) + {:error, error} -> {:error, error} end @@ -347,17 +369,20 @@ defmodule WebSockex do The callback `c:handle_connect/2` is invoked after the connection is established. """ - @spec start_link(url :: String.t | WebSockex.Conn.t, module, term, options) :: - {:ok, pid} | {:error, term} + @spec start_link(url :: String.t() | WebSockex.Conn.t(), module, term, options) :: + {:ok, pid} | {:error, term} def start_link(conn_info, module, state, opts \\ []) + def start_link(conn = %WebSockex.Conn{}, module, state, opts) do Utils.spawn(:link, conn, module, state, opts) end + def start_link(url, module, state, opts) do case WebSockex.Conn.parse_url(url) do {:ok, uri} -> conn = WebSockex.Conn.new(uri, opts) start_link(conn, module, state, opts) + {:error, error} -> {:error, error} end @@ -383,15 +408,18 @@ defmodule WebSockex do error tuple with a `WebSockex.ConnError` exception struct as the second element. """ - @spec send_frame(client, frame) :: :ok | - {:error, %WebSockex.FrameEncodeError{} - | %WebSockex.ConnError{} - | %WebSockex.NotConnectedError{} - | %WebSockex.InvalidFrameError{}} | - none + @spec send_frame(client, frame) :: + :ok + | {:error, + %WebSockex.FrameEncodeError{} + | %WebSockex.ConnError{} + | %WebSockex.NotConnectedError{} + | %WebSockex.InvalidFrameError{}} + | none def send_frame(client, _) when client == self() do raise %WebSockex.CallingSelfError{function: :send_frame} end + def send_frame(client, frame) do try do {:ok, res} = :gen.call(client, :"$websockex_send", frame) @@ -403,18 +431,17 @@ defmodule WebSockex do end @doc false - @spec init(pid, WebSockex.Conn.t, module, term, options) :: - {:ok, pid} | {:error, term} + @spec init(pid, WebSockex.Conn.t(), module, term, options) :: {:ok, pid} | {:error, term} def init(parent, conn, module, module_state, opts) do do_init(parent, self(), conn, module, module_state, opts) end - @spec init(pid, atom, WebSockex.Conn.t, module, term, options) :: - {:ok, pid} | {:error, term} + @spec init(pid, atom, WebSockex.Conn.t(), module, term, options) :: {:ok, pid} | {:error, term} def init(parent, name, conn, module, module_state, opts) do case Utils.register(name) do true -> do_init(parent, name, conn, module, module_state, opts) + {:error, _} = error -> :proc_lib.init_ack(parent, error) end @@ -426,9 +453,11 @@ defmodule WebSockex do def system_continue(parent, debug, %{connection_status: :connected} = state) do websocket_loop(parent, debug, Map.delete(state, :connection_status)) end + def system_continue(parent, debug, %{connection_status: :connecting} = state) do open_loop(parent, debug, Map.delete(state, :connection_status)) end + def system_continue(parent, debug, %{connection_status: {:closing, reason}} = state) do close_loop(reason, parent, debug, Map.delete(state, :connection_status)) end @@ -455,7 +484,9 @@ defmodule WebSockex do case apply(state.module, :code_change, [old_vsn, state.module_state, extra]) do {:ok, new_module_state} -> {:ok, %{state | module_state: new_module_state}} - other -> other + + other -> + other end catch other -> other @@ -466,13 +497,19 @@ defmodule WebSockex do log = :sys.get_debug(:log, debug, []) module_misc = module_status(opt, state.module, pdict, state.module_state) - [{:header, 'Status for WebSockex process #{inspect self()}'}, - {:data, [{"Status", sys_state}, - {"Parent", parent}, - {"Log", log}, - {"Connection Status", state.connection_status}, - {"Socket Buffer", state.buffer}, - {"Socket Module", state.module}]} | module_misc] + [ + {:header, 'Status for WebSockex process #{inspect(self())}'}, + {:data, + [ + {"Status", sys_state}, + {"Parent", parent}, + {"Log", log}, + {"Connection Status", state.connection_status}, + {"Socket Buffer", state.buffer}, + {"Socket Module", state.module} + ]} + | module_misc + ] end defp module_status(opt, module, pdict, module_state) do @@ -484,10 +521,14 @@ defmodule WebSockex do case result do {:"$EXIT", _} -> require Logger - Logger.error "There was an error while invoking #{module}.format_status/2" + Logger.error("There was an error while invoking #{module}.format_status/2") default - other when is_list(other) -> other - other -> [other] + + other when is_list(other) -> + other + + other -> + [other] end else default @@ -500,13 +541,15 @@ defmodule WebSockex do # OTP stuffs debug = Utils.parse_debug_options(self(), opts) - reply_fun = case Keyword.get(opts, :async, false) do - true -> - :proc_lib.init_ack(parent, {:ok, self()}) - &async_init_fun/1 - false -> - &sync_init_fun(parent, &1) - end + reply_fun = + case Keyword.get(opts, :async, false) do + true -> + :proc_lib.init_ack(parent, {:ok, self()}) + &async_init_fun/1 + + false -> + &sync_init_fun(parent, &1) + end state = %{ conn: conn, @@ -524,8 +567,10 @@ defmodule WebSockex do {:ok, new_state} -> debug = Utils.sys_debug(debug, :connected, state) module_init(parent, debug, new_state) + {:error, error, new_state} when handle_conn_failure == true -> init_conn_failure(error, parent, debug, new_state) + {:error, error, _} -> state.reply_fun.({:error, error}) end @@ -535,26 +580,35 @@ defmodule WebSockex do defp open_loop(parent, debug, state) do %{task: %{ref: ref}} = state + receive do {:system, from, req} -> state = Map.put(state, :connection_status, :connecting) :sys.handle_system_msg(req, from, parent, __MODULE__, debug, state) + {:"$websockex_send", from, _frame} -> :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :opening}}) open_loop(parent, debug, state) + {:EXIT, ^parent, reason} -> case state do %{reply_fun: reply_fun} -> reply_fun.(reason) exit(reason) + _ -> terminate(reason, parent, debug, state) end + {^ref, {:ok, new_conn}} -> Process.demonitor(ref, [:flush]) - new_state = Map.delete(state, :task) - |> Map.put(:conn, new_conn) + + new_state = + Map.delete(state, :task) + |> Map.put(:conn, new_conn) + {:ok, new_state} + {^ref, {:error, reason}} -> Process.demonitor(ref, [:flush]) new_state = Map.delete(state, :task) @@ -567,27 +621,36 @@ defmodule WebSockex do {:ok, frame, buffer} -> debug = Utils.sys_debug(debug, {:in, :frame, frame}, state) handle_frame(frame, parent, debug, %{state | buffer: buffer}) + :incomplete -> transport = state.conn.transport socket = state.conn.socket + receive do {:system, from, req} -> state = Map.put(state, :connection_status, :connected) :sys.handle_system_msg(req, from, parent, __MODULE__, debug, state) + {:"$websockex_cast", msg} -> debug = Utils.sys_debug(debug, {:in, :cast, msg}, state) common_handle({:handle_cast, msg}, parent, debug, state) + {:"$websockex_send", from, frame} -> sync_send(frame, from, parent, debug, state) + {^transport, ^socket, message} -> buffer = <> websocket_loop(parent, debug, %{state | buffer: buffer}) + {:tcp_closed, ^socket} -> handle_close({:remote, :closed}, parent, debug, state) + {:ssl_closed, ^socket} -> handle_close({:remote, :closed}, parent, debug, state) + {:EXIT, ^parent, reason} -> terminate(reason, parent, debug, state) + msg -> debug = Utils.sys_debug(debug, {:in, :msg, msg}, state) common_handle({:handle_info, msg}, parent, debug, state) @@ -598,23 +661,29 @@ defmodule WebSockex do defp close_loop(reason, parent, debug, %{conn: conn, timer_ref: timer_ref} = state) do transport = state.conn.transport socket = state.conn.socket + receive do {:system, from, req} -> state = Map.put(state, :connection_status, {:closing, reason}) :sys.handle_system_msg(req, from, parent, __MODULE__, debug, state) + {:EXIT, ^parent, reason} -> terminate(reason, parent, debug, state) + {^transport, ^socket, _} -> close_loop(reason, parent, debug, state) + {:"$websockex_send", from, _frame} -> :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :closing}}) close_loop(reason, parent, debug, state) + {:tcp_closed, ^socket} -> new_conn = %{conn | socket: nil} debug = Utils.sys_debug(debug, :closed, state) - purge_timer(timer_ref, :"websockex_close_timeout") + purge_timer(timer_ref, :websockex_close_timeout) state = Map.delete(state, :timer_ref) on_disconnect(reason, parent, debug, %{state | conn: new_conn}) + :"$websockex_close_timeout" -> new_conn = WebSockex.Conn.close_socket(conn) debug = Utils.sys_debug(debug, :timeout_closed, state) @@ -627,30 +696,39 @@ defmodule WebSockex do defp handle_frame(:ping, parent, debug, state) do common_handle({:handle_ping, :ping}, parent, debug, state) end + defp handle_frame({:ping, msg}, parent, debug, state) do common_handle({:handle_ping, {:ping, msg}}, parent, debug, state) end + defp handle_frame(:pong, parent, debug, state) do common_handle({:handle_pong, :pong}, parent, debug, state) end + defp handle_frame({:pong, msg}, parent, debug, state) do common_handle({:handle_pong, {:pong, msg}}, parent, debug, state) end + defp handle_frame(:close, parent, debug, state) do handle_close({:remote, :normal}, parent, debug, state) end + defp handle_frame({:close, code, reason}, parent, debug, state) do handle_close({:remote, code, reason}, parent, debug, state) end + defp handle_frame({:fragment, _, _} = fragment, parent, debug, state) do handle_fragment(fragment, parent, debug, state) end + defp handle_frame({:continuation, _} = fragment, parent, debug, state) do handle_fragment(fragment, parent, debug, state) end + defp handle_frame({:finish, _} = fragment, parent, debug, state) do handle_fragment(fragment, parent, debug, state) end + defp handle_frame(frame, parent, debug, state) do common_handle({:handle_frame, frame}, parent, debug, state) end @@ -658,15 +736,29 @@ defmodule WebSockex do defp handle_fragment({:fragment, type, part}, parent, debug, %{fragment: nil} = state) do websocket_loop(parent, debug, %{state | fragment: {type, part}}) end + defp handle_fragment({:fragment, _, _}, parent, debug, state) do - handle_close({:local, 1002, "Endpoint tried to start a fragment without finishing another"}, parent, debug, state) + handle_close( + {:local, 1002, "Endpoint tried to start a fragment without finishing another"}, + parent, + debug, + state + ) end + defp handle_fragment({:continuation, _}, parent, debug, %{fragment: nil} = state) do - handle_close({:local, 1002, "Endpoint sent a continuation frame without starting a fragment"}, parent, debug, state) + handle_close( + {:local, 1002, "Endpoint sent a continuation frame without starting a fragment"}, + parent, + debug, + state + ) end + defp handle_fragment({:continuation, next}, parent, debug, %{fragment: {type, part}} = state) do websocket_loop(parent, debug, %{state | fragment: {type, <>}}) end + defp handle_fragment({:finish, next}, parent, debug, %{fragment: {type, part}} = state) do frame = {type, <>} debug = Utils.sys_debug(debug, {:in, :completed_fragment, frame}, state) @@ -678,18 +770,23 @@ defmodule WebSockex do new_conn = %{state.conn | socket: nil} on_disconnect(reason, parent, debug, %{state | conn: new_conn}) end + defp handle_close({:remote, _} = reason, parent, debug, state) do handle_remote_close(reason, parent, debug, state) end + defp handle_close({:remote, _, _} = reason, parent, debug, state) do handle_remote_close(reason, parent, debug, state) end + defp handle_close({:local, _} = reason, parent, debug, state) do handle_local_close(reason, parent, debug, state) end + defp handle_close({:local, _, _} = reason, parent, debug, state) do handle_local_close(reason, parent, debug, state) end + defp handle_close({:error, _} = reason, parent, debug, state) do handle_error_close(reason, parent, debug, state) end @@ -700,26 +797,42 @@ defmodule WebSockex do case result do {:ok, new_state} -> websocket_loop(parent, debug, %{state | module_state: new_state}) + {:reply, frame, new_state} -> # A `with` that includes `else` clause isn't tail recursive (elixir-lang/elixir#6251) - res = with {:ok, binary_frame} <- WebSockex.Frame.encode_frame(frame), - do: WebSockex.Conn.socket_send(state.conn, binary_frame) + res = + with {:ok, binary_frame} <- WebSockex.Frame.encode_frame(frame), + do: WebSockex.Conn.socket_send(state.conn, binary_frame) + case res do :ok -> debug = Utils.sys_debug(debug, {:reply, function, frame}, state) websocket_loop(parent, debug, %{state | module_state: new_state}) + {:error, error} -> handle_close({:error, error}, parent, debug, %{state | module_state: new_state}) end + {:close, new_state} -> handle_close({:local, :normal}, parent, debug, %{state | module_state: new_state}) + {:close, {close_code, message}, new_state} -> - handle_close({:local, close_code, message}, parent, debug, %{state | module_state: new_state}) + handle_close({:local, close_code, message}, parent, debug, %{ + state + | module_state: new_state + }) + {:"$EXIT", reason} -> handle_terminate_close(reason, parent, debug, state) + badreply -> - error = %WebSockex.BadResponseError{module: state.module, function: function, - args: [msg, state.module_state], response: badreply} + error = %WebSockex.BadResponseError{ + module: state.module, + function: function, + args: [msg, state.module_state], + response: badreply + } + terminate(error, parent, debug, state) end end @@ -728,10 +841,11 @@ defmodule WebSockex do debug = Utils.sys_debug(debug, {:close, :remote, reason}, state) # If the socket is already closed then that's ok, but the spec says to send # the close frame back in response to receiving it. - debug = case send_close_frame(reason, state.conn) do - :ok -> Utils.sys_debug(debug, {:socket_out, :close, reason}, state) - _ -> debug - end + debug = + case send_close_frame(reason, state.conn) do + :ok -> Utils.sys_debug(debug, {:socket_out, :close, reason}, state) + _ -> debug + end timer_ref = Process.send_after(self(), :"$websockex_close_timeout", 5000) close_loop(reason, parent, debug, Map.put(state, :timer_ref, timer_ref)) @@ -739,11 +853,13 @@ defmodule WebSockex do defp handle_local_close(reason, parent, debug, state) do debug = Utils.sys_debug(debug, {:close, :local, reason}, state) + case send_close_frame(reason, state.conn) do :ok -> debug = Utils.sys_debug(debug, {:socket_out, :close, reason}, state) timer_ref = Process.send_after(self(), :"$websockex_close_timeout", 5000) close_loop(reason, parent, debug, Map.put(state, :timer_ref, timer_ref)) + {:error, %WebSockex.ConnError{original: reason}} when reason in [:closed, :einval] -> handle_close({:remote, :closed}, parent, debug, state) end @@ -760,10 +876,11 @@ defmodule WebSockex do def handle_terminate_close(reason, parent, debug, state) do debug = Utils.sys_debug(debug, {:close, :error, reason}, state) - debug = case send_close_frame(:error, state.conn) do - :ok -> Utils.sys_debug(debug, {:socket_out, :close, :error}, state) - _ -> debug - end + debug = + case send_close_frame(:error, state.conn) do + :ok -> Utils.sys_debug(debug, {:socket_out, :close, :error}, state) + _ -> debug + end # I'm not supposed to do this, but I'm going to go ahead and close the # socket here. If people complain I'll come up with something else. @@ -774,17 +891,20 @@ defmodule WebSockex do # Frame Sending defp sync_send(frame, from, parent, debug, %{conn: conn} = state) do - res = with {:ok, binary_frame} <- WebSockex.Frame.encode_frame(frame), - do: WebSockex.Conn.socket_send(conn, binary_frame) + res = + with {:ok, binary_frame} <- WebSockex.Frame.encode_frame(frame), + do: WebSockex.Conn.socket_send(conn, binary_frame) case res do :ok -> :gen.reply(from, :ok) debug = Utils.sys_debug(debug, {:socket_out, :sync_send, frame}, state) websocket_loop(parent, debug, state) + {:error, %WebSockex.ConnError{original: reason}} = error when reason in [:closed, :einval] -> :gen.reply(from, error) handle_close(error, parent, debug, state) + {:error, _} = error -> :gen.reply(from, error) websocket_loop(parent, debug, state) @@ -793,15 +913,17 @@ defmodule WebSockex do defp send_close_frame(reason, conn) do with {:ok, binary_frame} <- build_close_frame(reason), - do: WebSockex.Conn.socket_send(conn, binary_frame) + do: WebSockex.Conn.socket_send(conn, binary_frame) end defp build_close_frame({_, :normal}) do WebSockex.Frame.encode_frame(:close) end + defp build_close_frame({_, code, msg}) do WebSockex.Frame.encode_frame({:close, code, msg}) end + defp build_close_frame(:error) do WebSockex.Frame.encode_frame({:close, 1011, ""}) end @@ -812,16 +934,20 @@ defmodule WebSockex do case handle_disconnect(reason, state, attempt) do {:ok, new_module_state} -> init_failure(reason, parent, debug, %{state | module_state: new_module_state}) + {:reconnect, new_conn, new_module_state} -> state = %{state | conn: new_conn, module_state: new_module_state} debug = Utils.sys_debug(debug, :reconnect, state) + case open_connection(parent, debug, state) do {:ok, new_state} -> debug = Utils.sys_debug(debug, :connected, state) module_init(parent, debug, new_state) + {:error, new_reason, new_state} -> - init_conn_failure(new_reason, parent, debug, new_state, attempt+1) + init_conn_failure(new_reason, parent, debug, new_state, attempt + 1) end + {:"$EXIT", reason} -> init_failure(reason, parent, debug, state) end @@ -831,20 +957,25 @@ defmodule WebSockex do case handle_disconnect(reason, state, attempt) do {:ok, new_module_state} when is_tuple(reason) and elem(reason, 0) == :error -> terminate(elem(reason, 1), parent, debug, %{state | module_state: new_module_state}) + {:ok, new_module_state} -> terminate(reason, parent, debug, %{state | module_state: new_module_state}) + {:reconnect, new_conn, new_module_state} -> state = %{state | conn: new_conn, module_state: new_module_state} debug = Utils.sys_debug(debug, :reconnect, state) + case open_connection(parent, debug, state) do {:ok, new_state} -> debug = Utils.sys_debug(debug, :reconnected, state) reconnect(parent, debug, new_state) + {:error, new_reason, new_state} -> - on_disconnect(new_reason, parent, debug, new_state, attempt+1) + on_disconnect(new_reason, parent, debug, new_state, attempt + 1) end + {:"$EXIT", reason} -> - terminate(reason, parent, debug, state) + terminate(reason, parent, debug, state) end end @@ -853,16 +984,20 @@ defmodule WebSockex do case result do {:ok, new_module_state} -> - state = Map.merge(state, %{buffer: <<>>, - fragment: nil, - module_state: new_module_state}) - websocket_loop(parent, debug, state) + state = Map.merge(state, %{buffer: <<>>, fragment: nil, module_state: new_module_state}) + websocket_loop(parent, debug, state) + {:"$EXIT", reason} -> terminate(reason, parent, debug, state) + badreply -> - reason = %WebSockex.BadResponseError{module: state.module, - function: :handle_connect, args: [state.conn, state.module_state], - response: badreply} + reason = %WebSockex.BadResponseError{ + module: state.module, + function: :handle_connect, + args: [state.conn, state.module_state], + response: badreply + } + terminate(reason, parent, debug, state) end end @@ -870,19 +1005,21 @@ defmodule WebSockex do defp open_connection(parent, debug, %{conn: conn} = state) do my_pid = self() debug = Utils.sys_debug(debug, :connect, state) - task = Task.async(fn -> - with {:ok, conn} <- WebSockex.Conn.open_socket(conn), - key <- :crypto.strong_rand_bytes(16) |> Base.encode64, - {:ok, request} <- WebSockex.Conn.build_request(conn, key), - :ok <- WebSockex.Conn.socket_send(conn, request), - {:ok, headers} <- WebSockex.Conn.handle_response(conn), - :ok <- validate_handshake(headers, key) - do - :ok = WebSockex.Conn.controlling_process(conn, my_pid) - :ok = WebSockex.Conn.set_active(conn) - {:ok, %{conn | resp_headers: headers}} - end - end) + + task = + Task.async(fn -> + with {:ok, conn} <- WebSockex.Conn.open_socket(conn), + key <- :crypto.strong_rand_bytes(16) |> Base.encode64(), + {:ok, request} <- WebSockex.Conn.build_request(conn, key), + :ok <- WebSockex.Conn.socket_send(conn, request), + {:ok, headers} <- WebSockex.Conn.handle_response(conn), + :ok <- validate_handshake(headers, key) do + :ok = WebSockex.Conn.controlling_process(conn, my_pid) + :ok = WebSockex.Conn.set_active(conn) + {:ok, %{conn | resp_headers: headers}} + end + end) + open_loop(parent, debug, Map.put(state, :task, task)) end @@ -893,58 +1030,78 @@ defmodule WebSockex do case result do {:ok, new_module_state} -> - state.reply_fun.({:ok, self()}) - state = Map.put(state, :module_state, new_module_state) - |> Map.delete(:reply_fun) + state.reply_fun.({:ok, self()}) + + state = + Map.put(state, :module_state, new_module_state) + |> Map.delete(:reply_fun) + + websocket_loop(parent, debug, state) - websocket_loop(parent, debug, state) {:"$EXIT", reason} -> state.reply_fun.(reason) + badreply -> - reason = {:error, %WebSockex.BadResponseError{module: state.module, - function: :handle_connect, args: [state.conn, state.module_state], - response: badreply}} + reason = + {:error, + %WebSockex.BadResponseError{ + module: state.module, + function: :handle_connect, + args: [state.conn, state.module_state], + response: badreply + }} + state.reply_fun.(reason) end end @spec terminate(any, pid, any, any) :: no_return defp terminate(reason, parent, debug, %{conn: %{socket: socket}} = state) - when not is_nil(socket) do + when not is_nil(socket) do handle_terminate_close(reason, parent, debug, state) end + defp terminate(reason, _parent, _debug, %{module: mod, module_state: mod_state}) do mod.terminate(reason, mod_state) + case reason do {_, :normal} -> exit(:normal) + {_, 1000, _} -> exit(:normal) + _ -> exit(reason) end end defp handle_disconnect(reason, state, attempt) do - status_map = %{conn: state.conn, - reason: reason, - attempt_number: attempt} + status_map = %{conn: state.conn, reason: reason, attempt_number: attempt} result = try_callback(state.module, :handle_disconnect, [status_map, state.module_state]) case result do {:ok, new_state} -> {:ok, new_state} + {:reconnect, new_state} -> {:reconnect, state.conn, new_state} + {:reconnect, new_conn, new_state} -> {:reconnect, new_conn, new_state} + {:"$EXIT", _} = res -> res + badreply -> - {:"$EXIT", %WebSockex.BadResponseError{module: state.module, - function: :handle_disconnect, args: [status_map, state.module_state], - response: badreply}} + {:"$EXIT", + %WebSockex.BadResponseError{ + module: state.module, + function: :handle_disconnect, + args: [status_map, state.module_state], + response: badreply + }} end end @@ -957,6 +1114,7 @@ defmodule WebSockex do stacktrace = System.stacktrace() reason = Exception.normalize(:error, payload, stacktrace) {:"$EXIT", {reason, stacktrace}} + :exit, payload -> {:"$EXIT", payload} end @@ -971,12 +1129,13 @@ defmodule WebSockex do defp sync_init_fun(parent, {error, stacktrace}) when is_list(stacktrace) do :proc_lib.init_ack(parent, {:error, error}) end + defp sync_init_fun(parent, reply) do :proc_lib.init_ack(parent, reply) end defp validate_handshake(headers, key) do - challenge = :crypto.hash(:sha, key <> @handshake_guid) |> Base.encode64 + challenge = :crypto.hash(:sha, key <> @handshake_guid) |> Base.encode64() {_, res} = List.keyfind(headers, "Sec-Websocket-Accept", 0) @@ -989,7 +1148,9 @@ defmodule WebSockex do defp purge_timer(ref, msg) do case Process.cancel_timer(ref) do - i when is_integer(i) -> :ok + i when is_integer(i) -> + :ok + false -> receive do ^msg -> :ok diff --git a/lib/websockex/application.ex b/lib/websockex/application.ex index ea60be8..e6111eb 100644 --- a/lib/websockex/application.ex +++ b/lib/websockex/application.ex @@ -11,6 +11,6 @@ defmodule WebSockex.Application do unless URI.default_port("wss"), do: URI.default_port("wss", 443) # Start an empty supervisor for OTP - Supervisor.start_link([], [strategy: :one_for_one, name: WebSockex.Supervisor]) + Supervisor.start_link([], strategy: :one_for_one, name: WebSockex.Supervisor) end end diff --git a/lib/websockex/conn.ex b/lib/websockex/conn.ex index 0658bf1..f4b5f14 100644 --- a/lib/websockex/conn.ex +++ b/lib/websockex/conn.ex @@ -24,11 +24,11 @@ defmodule WebSockex.Conn do insecure: true, resp_headers: [] - @type socket :: :gen_tcp.socket | :ssl.sslsocket - @type header :: {field :: String.t, value :: String.t} + @type socket :: :gen_tcp.socket() | :ssl.sslsocket() + @type header :: {field :: String.t(), value :: String.t()} @type transport :: :tcp | :ssl - @type certification :: :public_key.der_encoded + @type certification :: :public_key.der_encoded() @typedoc """ Options used when establishing a tcp or ssl connection. @@ -48,45 +48,53 @@ defmodule WebSockex.Conn do [public_key]: http://erlang.org/doc/apps/public_key/using_public_key.html """ - @type connection_option :: {:extra_headers, [header]} | - {:cacerts, [certification]} | - {:insecure, boolean} | - {:socket_connect_timeout, non_neg_integer}| - {:socket_recv_timeout, non_neg_integer} - - @type t :: %__MODULE__{conn_mod: :gen_tcp | :ssl, - host: String.t, - port: non_neg_integer, - path: String.t, - query: String.t | nil, - extra_headers: [header], - transport: transport, - socket: socket | nil, - socket_connect_timeout: non_neg_integer, - socket_recv_timeout: non_neg_integer, - resp_headers: [header]} + @type connection_option :: + {:extra_headers, [header]} + | {:cacerts, [certification]} + | {:insecure, boolean} + | {:socket_connect_timeout, non_neg_integer} + | {:socket_recv_timeout, non_neg_integer} + + @type t :: %__MODULE__{ + conn_mod: :gen_tcp | :ssl, + host: String.t(), + port: non_neg_integer, + path: String.t(), + query: String.t() | nil, + extra_headers: [header], + transport: transport, + socket: socket | nil, + socket_connect_timeout: non_neg_integer, + socket_recv_timeout: non_neg_integer, + resp_headers: [header] + } @doc """ Returns a new `WebSockex.Conn` struct from a uri and options. """ - @spec new(url :: String.t | URI.t, [connection_option]) :: - __MODULE__.t | {:error, %WebSockex.URLError{}} + @spec new(url :: String.t() | URI.t(), [connection_option]) :: + __MODULE__.t() | {:error, %WebSockex.URLError{}} def new(url, opts \\ []) + def new(%URI{} = uri, opts) do mod = conn_module(uri.scheme) - %WebSockex.Conn{host: uri.host, - port: uri.port, - path: uri.path, - query: uri.query, - conn_mod: mod, - transport: transport(mod), - extra_headers: Keyword.get(opts, :extra_headers, []), - cacerts: Keyword.get(opts, :cacerts, nil), - insecure: Keyword.get(opts, :insecure, true), - socket_connect_timeout: Keyword.get(opts, :socket_connect_timeout, @socket_connect_timeout_default), - socket_recv_timeout: Keyword.get(opts, :socket_recv_timeout, @socket_recv_timeout_default)} + %WebSockex.Conn{ + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + conn_mod: mod, + transport: transport(mod), + extra_headers: Keyword.get(opts, :extra_headers, []), + cacerts: Keyword.get(opts, :cacerts, nil), + insecure: Keyword.get(opts, :insecure, true), + socket_connect_timeout: + Keyword.get(opts, :socket_connect_timeout, @socket_connect_timeout_default), + socket_recv_timeout: Keyword.get(opts, :socket_recv_timeout, @socket_recv_timeout_default) + } end + def new(url, opts) do case parse_url(url) do {:ok, %URI{} = uri} -> new(uri, opts) @@ -97,21 +105,24 @@ defmodule WebSockex.Conn do @doc """ Parses a url string for a valid URI """ - @spec parse_url(String.t) :: {:ok, URI.t} | {:error, %WebSockex.URLError{}} + @spec parse_url(String.t()) :: {:ok, URI.t()} | {:error, %WebSockex.URLError{}} def parse_url(url) do case URI.parse(url) do %URI{port: port, scheme: protocol} when protocol in ["ws", "wss"] and is_nil(port) -> # Someone may have deleted the URI config but I'm going to assume it's # just that the application didn't get them registered. {:error, %WebSockex.ApplicationError{reason: :not_started}} + # This is confusing to look at. But it's just a match with multiple guards %URI{host: host, port: port, scheme: protocol} when is_nil(host) when is_nil(port) - when not protocol in ["ws", "wss", "http", "https"] -> + when not (protocol in ["ws", "wss", "http", "https"]) -> {:error, %WebSockex.URLError{url: url}} + %URI{path: nil} = uri -> {:ok, %{uri | path: "/"}} + %URI{} = uri -> {:ok, uri} end @@ -120,7 +131,7 @@ defmodule WebSockex.Conn do @doc """ Sends data using the `conn_mod` module. """ - @spec socket_send(__MODULE__.t, binary) :: :ok | {:error, reason :: term} + @spec socket_send(__MODULE__.t(), binary) :: :ok | {:error, reason :: term} def socket_send(conn, message) do case conn.conn_mod.send(conn.socket, message) do :ok -> :ok @@ -131,26 +142,34 @@ defmodule WebSockex.Conn do @doc """ Opens a socket to a uri and returns a conn struct. """ - @spec open_socket(__MODULE__.t) :: {:ok, __MODULE__.t} | {:error, term} + @spec open_socket(__MODULE__.t()) :: {:ok, __MODULE__.t()} | {:error, term} def open_socket(conn) + def open_socket(%{conn_mod: :gen_tcp} = conn) do - case :gen_tcp.connect(String.to_charlist(conn.host), - conn.port, - [:binary, active: false, packet: 0], - conn.socket_connect_timeout) do + case :gen_tcp.connect( + String.to_charlist(conn.host), + conn.port, + [:binary, active: false, packet: 0], + conn.socket_connect_timeout + ) do {:ok, socket} -> {:ok, Map.put(conn, :socket, socket)} + {:error, error} -> {:error, %WebSockex.ConnError{original: error}} end end + def open_socket(%{conn_mod: :ssl} = conn) do - case :ssl.connect(String.to_charlist(conn.host), - conn.port, - ssl_connection_options(conn), - conn.socket_connect_timeout) do + case :ssl.connect( + String.to_charlist(conn.host), + conn.port, + ssl_connection_options(conn), + conn.socket_connect_timeout + ) do {:ok, socket} -> {:ok, Map.put(conn, :socket, socket)} + {:error, error} -> {:error, %WebSockex.ConnError{original: error}} end @@ -162,9 +181,10 @@ defmodule WebSockex.Conn do When the `:socket` field is `nil` in the struct, the function just returns the struct as is. """ - @spec close_socket(__MODULE__.t) :: %WebSockex.Conn{socket: nil} + @spec close_socket(__MODULE__.t()) :: %WebSockex.Conn{socket: nil} def close_socket(conn) def close_socket(%{socket: nil} = conn), do: conn + def close_socket(%{socket: socket} = conn) do conn.conn_mod.close(socket) %{conn | socket: nil} @@ -175,18 +195,22 @@ defmodule WebSockex.Conn do The key parameter is part of the websocket handshake process. """ - @spec build_request(__MODULE__.t, key :: String.t) :: {:ok, String.t} + @spec build_request(__MODULE__.t(), key :: String.t()) :: {:ok, String.t()} def build_request(conn, key) do - headers = [{"Host", conn.host}, - {"Connection", "Upgrade"}, - {"Upgrade", "websocket"}, - {"Sec-WebSocket-Version", "13"}, - {"Sec-WebSocket-Key", key}] ++ conn.extra_headers - |> Enum.map(&format_header/1) + headers = + ([ + {"Host", conn.host}, + {"Connection", "Upgrade"}, + {"Upgrade", "websocket"}, + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Key", key} + ] ++ conn.extra_headers) + |> Enum.map(&format_header/1) # Build the request - request = ["GET #{build_full_path(conn)} HTTP/1.1" | headers] - |> Enum.join("\r\n") + request = + ["GET #{build_full_path(conn)} HTTP/1.1" | headers] + |> Enum.join("\r\n") {:ok, request <> "\r\n\r\n"} end @@ -198,27 +222,29 @@ defmodule WebSockex.Conn do Sends any access information in the buffer back to the process as a message to be processed. """ - @spec handle_response(__MODULE__.t) :: - {:ok, [header]} | {:error, reason :: term} + @spec handle_response(__MODULE__.t()) :: {:ok, [header]} | {:error, reason :: term} def handle_response(conn) do with {:ok, buffer} <- wait_for_response(conn), {:ok, headers, buffer} <- decode_response(buffer) do - # Send excess buffer back to the process - unless buffer == "" do - send(self(), {transport(conn.conn_mod), conn.socket, buffer}) - end - {:ok, headers} - end + # Send excess buffer back to the process + unless buffer == "" do + send(self(), {transport(conn.conn_mod), conn.socket, buffer}) + end + + {:ok, headers} + end end @doc """ Sets the socket to active. """ - @spec set_active(__MODULE__.t, true | false) :: :ok | {:error, reason :: term} + @spec set_active(__MODULE__.t(), true | false) :: :ok | {:error, reason :: term} def set_active(conn, val \\ true) + def set_active(%{conn_mod: :gen_tcp} = conn, val) do :inet.setopts(conn.socket, active: val) end + def set_active(%{conn_mod: :ssl} = conn, val) do :ssl.setopts(conn.socket, active: val) end @@ -226,7 +252,7 @@ defmodule WebSockex.Conn do @doc """ Set the socket's controlling process. """ - @spec controlling_process(__MODULE__.t, new_owner :: pid) :: :ok | {:error, term} + @spec controlling_process(__MODULE__.t(), new_owner :: pid) :: :ok | {:error, term} def controlling_process(conn, new_owner) do conn.conn_mod.controlling_process(conn.socket, new_owner) end @@ -243,7 +269,9 @@ defmodule WebSockex.Conn do defp wait_for_response(conn, buffer \\ "") do case Regex.match?(~r/\r\n\r\n/, buffer) do - true -> {:ok, buffer} + true -> + {:ok, buffer} + false -> with {:ok, data} <- conn.conn_mod.recv(conn.socket, 0, conn.socket_recv_timeout) do wait_for_response(conn, buffer <> data) @@ -259,15 +287,17 @@ defmodule WebSockex.Conn do defp build_full_path(%__MODULE__{path: path, query: query}) do struct(URI, %{path: path, query: query}) - |> URI.to_string + |> URI.to_string() end defp decode_response(response) do case :erlang.decode_packet(:http_bin, response, []) do {:ok, {:http_response, _version, 101, _message}, rest} -> - decode_headers(rest) + decode_headers(rest) + {:ok, {:http_response, _, code, message}, _} -> {:error, %WebSockex.RequestError{code: code, message: message}} + {:error, error} -> {:error, error} end @@ -277,6 +307,7 @@ defmodule WebSockex.Conn do case :erlang.decode_packet(:httph_bin, rest, []) do {:ok, {:http_header, _len, field, _res, value}, rest} -> decode_headers(rest, [{field, value} | headers]) + {:ok, :http_eoh, body} -> {:ok, headers, body} end @@ -292,6 +323,7 @@ defmodule WebSockex.Conn do verify: :verify_none ] end + defp ssl_connection_options(%{cacerts: cacerts}) when cacerts != nil do [ :binary, diff --git a/lib/websockex/errors.ex b/lib/websockex/errors.ex index 78d42b0..725ff24 100644 --- a/lib/websockex/errors.ex +++ b/lib/websockex/errors.ex @@ -14,15 +14,19 @@ defmodule WebSockex.ConnError do @moduledoc false defexception [:original] - def message(%__MODULE__{original: :nxdomain}), do: "Connection Error: Could not resolve domain name." - def message(%__MODULE__{original: error}), do: "Connection Error: #{inspect error}" + def message(%__MODULE__{original: :nxdomain}), + do: "Connection Error: Could not resolve domain name." + + def message(%__MODULE__{original: error}), do: "Connection Error: #{inspect(error)}" end defmodule WebSockex.RequestError do defexception [:code, :message] def message(%__MODULE__{code: code, message: message}) do - "Didn't get a proper response from the server. The response was: #{inspect code} #{inspect message}" + "Didn't get a proper response from the server. The response was: #{inspect(code)} #{ + inspect(message) + }" end end @@ -30,7 +34,7 @@ defmodule WebSockex.URLError do @moduledoc false defexception [:url] - def message(%__MODULE__{url: url}), do: "Invalid URL: #{inspect url}" + def message(%__MODULE__{url: url}), do: "Invalid URL: #{inspect(url)}" end defmodule WebSockex.HandshakeError do @@ -38,9 +42,11 @@ defmodule WebSockex.HandshakeError do defexception [:challenge, :response] def message(%__MODULE__{challenge: challenge, response: response}) do - ["Handshake Failed: Response didn't match challenge.", - "Response: #{inspect response}", - "Challenge: #{inspect challenge}"] + [ + "Handshake Failed: Response didn't match challenge.", + "Response: #{inspect(response)}", + "Challenge: #{inspect(challenge)}" + ] |> Enum.join("\n") end end @@ -50,7 +56,9 @@ defmodule WebSockex.BadResponseError do defexception [:response, :module, :function, :args] def message(%__MODULE__{} = error) do - "Bad Response: Got #{inspect error.response} from #{inspect Exception.format_mfa(error.module, error.function, error.args)}" + "Bad Response: Got #{inspect(error.response)} from #{ + inspect(Exception.format_mfa(error.module, error.function, error.args)) + }" end end @@ -60,17 +68,25 @@ defmodule WebSockex.FrameError do def message(%__MODULE__{reason: :nonfin_control_frame} = exception) do "Fragmented Control Frame: Control Frames Can't Be Fragmented\nbuffer: #{exception.buffer}" end + def message(%__MODULE__{reason: :control_frame_too_large} = exception) do - "Control Frame Too Large: Control Frames Can't Be Larger Than 125 Bytes\nbuffer: #{exception.buffer}" + "Control Frame Too Large: Control Frames Can't Be Larger Than 125 Bytes\nbuffer: #{ + exception.buffer + }" end + def message(%__MODULE__{reason: :invalid_utf8} = exception) do "Invalid UTF-8: Text and Close frames must have UTF-8 payloads.\nbuffer: #{exception.buffer}" end + def message(%__MODULE__{reason: :invalid_close_code} = exception) do - "Invalid Close Code: Close Codes must be in range of 1000 through 4999\nbuffer: #{exception.buffer}" + "Invalid Close Code: Close Codes must be in range of 1000 through 4999\nbuffer: #{ + exception.buffer + }" end + def message(%__MODULE__{} = exception) do - "Frame Error: #{inspect exception}" + "Frame Error: #{inspect(exception)}" end end @@ -80,13 +96,16 @@ defmodule WebSockex.FrameEncodeError do def message(%__MODULE__{reason: :control_frame_too_large} = error) do """ Control frame payload too large: Payload must be less than 126 bytes. - Frame: {#{inspect error.frame_type}, #{inspect error.frame_payload}} + Frame: {#{inspect(error.frame_type)}, #{inspect(error.frame_payload)}} """ end + def message(%__MODULE__{reason: :close_code_out_of_range} = error) do """ Close Code Out of Range: Close code must be between 1000-4999. - Frame: {#{inspect error.frame_type}, #{inspect error.close_code}, #{inspect error.frame_payload}} + Frame: {#{inspect(error.frame_type)}, #{inspect(error.close_code)}, #{ + inspect(error.frame_payload) + }} """ end end @@ -95,7 +114,7 @@ defmodule WebSockex.InvalidFrameError do defexception [:frame] def message(%__MODULE__{frame: frame}) do - "The following frame is an invalid frame: #{inspect frame}" + "The following frame is an invalid frame: #{inspect(frame)}" end end @@ -105,8 +124,8 @@ defmodule WebSockex.FragmentParseError do def message(%__MODULE__{reason: :two_start_frames} = error) do """ Cannot Add Another Start Frame to a Existing Fragment. - Fragment: #{inspect error.fragment} - Continuation: #{inspect error.continuation} + Fragment: #{inspect(error.fragment)} + Continuation: #{inspect(error.continuation)} """ end end @@ -129,6 +148,7 @@ defmodule WebSockex.CallingSelfError do The function send_frame/2 cannot be used inside of a callback. Instead try returning {:reply, frame, state} from the callback. """ end + def message(%__MODULE__{}) do "Process attempted to call itself." end diff --git a/lib/websockex/frame.ex b/lib/websockex/frame.ex index e336c9f..4686c7c 100644 --- a/lib/websockex/frame.ex +++ b/lib/websockex/frame.ex @@ -14,86 +14,113 @@ defmodule WebSockex.Frame do @typedoc "This is required to be valid UTF-8" @type utf8 :: binary - @type frame :: :ping | :pong | :close | {:ping, binary} | {:pong, binary} | - {:close, close_code, utf8} | {:text, utf8} | {:binary, binary} | - {:fragment, :text | :binary, binary} | {:continuation, binary} | - {:finish, binary} + @type frame :: + :ping + | :pong + | :close + | {:ping, binary} + | {:pong, binary} + | {:close, close_code, utf8} + | {:text, utf8} + | {:binary, binary} + | {:fragment, :text | :binary, binary} + | {:continuation, binary} + | {:finish, binary} - @opcodes %{text: 1, - binary: 2, - close: 8, - ping: 9, - pong: 10} + @opcodes %{text: 1, binary: 2, close: 8, ping: 9, pong: 10} @doc """ Parses a bitstring and returns a frame. """ @spec parse_frame(bitstring) :: - :incomplete | {:ok, frame, buffer} | {:error, %WebSockex.FrameError{}} + :incomplete | {:ok, frame, buffer} | {:error, %WebSockex.FrameError{}} def parse_frame(data) when bit_size(data) < 16 do :incomplete end + for {key, opcode} <- Map.take(@opcodes, [:close, :ping, :pong]) do # Control Codes can have 0 length payloads def parse_frame(<<1::1, 0::3, unquote(opcode)::4, 0::1, 0::7, buffer::bitstring>>) do {:ok, unquote(key), buffer} end + # Large Control Frames def parse_frame(<<1::1, 0::3, unquote(opcode)::4, 0::1, 126::7, _::bitstring>> = buffer) do - {:error, %WebSockex.FrameError{reason: :control_frame_too_large, - opcode: unquote(key), - buffer: buffer}} + {:error, + %WebSockex.FrameError{ + reason: :control_frame_too_large, + opcode: unquote(key), + buffer: buffer + }} end + def parse_frame(<<1::1, 0::3, unquote(opcode)::4, 0::1, 127::7, _::bitstring>> = buffer) do - {:error, %WebSockex.FrameError{reason: :control_frame_too_large, - opcode: unquote(key), - buffer: buffer}} + {:error, + %WebSockex.FrameError{ + reason: :control_frame_too_large, + opcode: unquote(key), + buffer: buffer + }} end + # Nonfin Control Frames def parse_frame(<<0::1, 0::3, unquote(opcode)::4, 0::1, _::7, _::bitstring>> = buffer) do - {:error, %WebSockex.FrameError{reason: :nonfin_control_frame, - opcode: unquote(key), - buffer: buffer}} + {:error, + %WebSockex.FrameError{reason: :nonfin_control_frame, opcode: unquote(key), buffer: buffer}} end end + # Incomplete Frames def parse_frame(<<_::9, len::7, remaining::bitstring>>) when byte_size(remaining) < len do :incomplete end + for {_key, opcode} <- Map.take(@opcodes, [:text, :binary]) do - def parse_frame(<<_::1, 0::3, unquote(opcode)::4, 0::1, 126::7, len::16, remaining::bitstring>>) - when byte_size(remaining) < len do + def parse_frame( + <<_::1, 0::3, unquote(opcode)::4, 0::1, 126::7, len::16, remaining::bitstring>> + ) + when byte_size(remaining) < len do :incomplete end - def parse_frame(<<_::1, 0::3, unquote(opcode)::4, 0::1, 127::7, len::64, remaining::bitstring>>) - when byte_size(remaining) < len do + + def parse_frame( + <<_::1, 0::3, unquote(opcode)::4, 0::1, 127::7, len::64, remaining::bitstring>> + ) + when byte_size(remaining) < len do :incomplete end end + # Close Frame with Single Byte def parse_frame(<<1::1, 0::3, 8::4, 0::1, 1::7, _::bitstring>> = buffer) do - {:error, %WebSockex.FrameError{reason: :close_with_single_byte_payload, - opcode: :close, - buffer: buffer}} + {:error, + %WebSockex.FrameError{ + reason: :close_with_single_byte_payload, + opcode: :close, + buffer: buffer + }} end + # Parse Close Frames with Payloads - def parse_frame(<<1::1, 0::3, 8::4, 0::1, len::7, close_code::integer-size(16), remaining::bitstring>> = buffer) - when close_code in 1000..4999 do + def parse_frame( + <<1::1, 0::3, 8::4, 0::1, len::7, close_code::integer-size(16), remaining::bitstring>> = + buffer + ) + when close_code in 1000..4999 do size = len - 2 <> = remaining + if String.valid?(payload) do {:ok, {:close, close_code, payload}, rest} else - {:error, %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :close, - buffer: buffer}} + {:error, %WebSockex.FrameError{reason: :invalid_utf8, opcode: :close, buffer: buffer}} end end + def parse_frame(<<1::1, 0::3, 8::4, _::bitstring>> = buffer) do - {:error, %WebSockex.FrameError{reason: :invalid_close_code, - opcode: :close, - buffer: buffer}} + {:error, %WebSockex.FrameError{reason: :invalid_close_code, opcode: :close, buffer: buffer}} end + # Ping and Pong with Payloads for {key, opcode} <- Map.take(@opcodes, [:ping, :pong]) do def parse_frame(<<1::1, 0::3, unquote(opcode)::4, 0::1, len::7, remaining::bitstring>>) do @@ -101,66 +128,85 @@ defmodule WebSockex.Frame do {:ok, {unquote(key), payload}, rest} end end + # Text Frames (Check Valid UTF-8 Payloads) def parse_frame(<<1::1, 0::3, 1::4, 0::1, 126::7, len::16, remaining::bitstring>> = buffer) do parse_text_payload(len, remaining, buffer) end + def parse_frame(<<1::1, 0::3, 1::4, 0::1, 127::7, len::64, remaining::bitstring>> = buffer) do parse_text_payload(len, remaining, buffer) end + def parse_frame(<<1::1, 0::3, 1::4, 0::1, len::7, remaining::bitstring>> = buffer) do parse_text_payload(len, remaining, buffer) end + # Binary Frames def parse_frame(<<1::1, 0::3, 2::4, 0::1, 126::7, len::16, remaining::bitstring>>) do <> = remaining {:ok, {:binary, payload}, rest} end + def parse_frame(<<1::1, 0::3, 2::4, 0::1, 127::7, len::64, remaining::bitstring>>) do <> = remaining {:ok, {:binary, payload}, rest} end + def parse_frame(<<1::1, 0::3, 2::4, 0::1, len::7, remaining::bitstring>>) do <> = remaining {:ok, {:binary, payload}, rest} end + # Start of Fragmented Message for {key, opcode} <- Map.take(@opcodes, [:text, :binary]) do - def parse_frame(<<0::1, 0::3, unquote(opcode)::4, 0::1, 126::7, len::16, remaining::bitstring>>) do + def parse_frame( + <<0::1, 0::3, unquote(opcode)::4, 0::1, 126::7, len::16, remaining::bitstring>> + ) do <> = remaining {:ok, {:fragment, unquote(key), payload}, rest} end - def parse_frame(<<0::1, 0::3, unquote(opcode)::4, 0::1, 127::7, len::64, remaining::bitstring>>) do + + def parse_frame( + <<0::1, 0::3, unquote(opcode)::4, 0::1, 127::7, len::64, remaining::bitstring>> + ) do <> = remaining {:ok, {:fragment, unquote(key), payload}, rest} end + def parse_frame(<<0::1, 0::3, unquote(opcode)::4, 0::1, len::7, remaining::bitstring>>) do <> = remaining {:ok, {:fragment, unquote(key), payload}, rest} end end + # Parse Fragmentation Continuation Frames def parse_frame(<<0::1, 0::3, 0::4, 0::1, 126::7, len::16, remaining::bitstring>>) do <> = remaining {:ok, {:continuation, payload}, rest} end + def parse_frame(<<0::1, 0::3, 0::4, 0::1, 127::7, len::64, remaining::bitstring>>) do <> = remaining {:ok, {:continuation, payload}, rest} end + def parse_frame(<<0::1, 0::3, 0::4, 0::1, len::7, remaining::bitstring>>) do <> = remaining {:ok, {:continuation, payload}, rest} end + # Parse Fragmentation Finish Frames def parse_frame(<<1::1, 0::3, 0::4, 0::1, 126::7, len::16, remaining::bitstring>>) do <> = remaining {:ok, {:finish, payload}, rest} end + def parse_frame(<<1::1, 0::3, 0::4, 0::1, 127::7, len::64, remaining::bitstring>>) do <> = remaining {:ok, {:finish, payload}, rest} end + def parse_frame(<<1::1, 0::3, 0::4, 0::1, len::7, remaining::bitstring>>) do <> = remaining {:ok, {:finish, payload}, rest} @@ -169,35 +215,40 @@ defmodule WebSockex.Frame do @doc """ Parses and combines two frames in a fragmented segment. """ - @spec parse_fragment({:fragment, :text | :binary, binary}, - {:continuation | :finish, binary}) :: - {:fragment, :text | :binary, binary} | {:text | :binary, binary} | - {:error, %WebSockex.FragmentParseError{}} + @spec parse_fragment({:fragment, :text | :binary, binary}, {:continuation | :finish, binary}) :: + {:fragment, :text | :binary, binary} + | {:text | :binary, binary} + | {:error, %WebSockex.FragmentParseError{}} def parse_fragment(fragmented_parts, continuation_frame) + def parse_fragment({:fragment, _, _} = frame0, {:fragment, _, _} = frame1) do {:error, - %WebSockex.FragmentParseError{reason: :two_start_frames, - fragment: frame0, - continuation: frame1}} + %WebSockex.FragmentParseError{ + reason: :two_start_frames, + fragment: frame0, + continuation: frame1 + }} end + def parse_fragment({:fragment, type, fragment}, {:continuation, continuation}) do {:ok, {:fragment, type, <>}} end + def parse_fragment({:fragment, :binary, fragment}, {:finish, continuation}) do {:ok, {:binary, <>}} end + # Make sure text is valid UTF-8 def parse_fragment({:fragment, :text, fragment}, {:finish, continuation}) do text = <> + if String.valid?(text) do {:ok, {:text, text}} else - {:error, - %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :text, - buffer: text}} + {:error, %WebSockex.FrameError{reason: :invalid_utf8, opcode: :text, buffer: text}} end end + @doc """ Encodes a frame into a binary for sending. """ @@ -207,42 +258,57 @@ defmodule WebSockex.Frame do for {key, opcode} <- Map.take(@opcodes, [:ping, :pong]) do def encode_frame({unquote(key), <>}) when byte_size(payload) > 125 do {:error, - %WebSockex.FrameEncodeError{reason: :control_frame_too_large, - frame_type: unquote(key), - frame_payload: payload}} + %WebSockex.FrameEncodeError{ + reason: :control_frame_too_large, + frame_type: unquote(key), + frame_payload: payload + }} end + def encode_frame(unquote(key)) do mask = create_mask_key() {:ok, <<1::1, 0::3, unquote(opcode)::4, 1::1, 0::7, mask::bytes-size(4)>>} end + def encode_frame({unquote(key), <>}) do mask = create_mask_key() len = byte_size(payload) masked_payload = mask(mask, payload) - {:ok, <<1::1, 0::3, unquote(opcode)::4, 1::1, len::7, mask::bytes-size(4), masked_payload::binary-size(len)>>} + + {:ok, + <<1::1, 0::3, unquote(opcode)::4, 1::1, len::7, mask::bytes-size(4), + masked_payload::binary-size(len)>>} end end + # Encode Close Frames def encode_frame({:close, close_code, <>}) - when not close_code in 1000..4999 do + when not (close_code in 1000..4999) do {:error, - %WebSockex.FrameEncodeError{reason: :close_code_out_of_range, - frame_type: :close, - frame_payload: payload, - close_code: close_code}} + %WebSockex.FrameEncodeError{ + reason: :close_code_out_of_range, + frame_type: :close, + frame_payload: payload, + close_code: close_code + }} end + def encode_frame({:close, close_code, <>}) - when byte_size(payload) > 123 do + when byte_size(payload) > 123 do {:error, - %WebSockex.FrameEncodeError{reason: :control_frame_too_large, - frame_type: :close, - frame_payload: payload, - close_code: close_code}} + %WebSockex.FrameEncodeError{ + reason: :control_frame_too_large, + frame_type: :close, + frame_payload: payload, + close_code: close_code + }} end + def encode_frame(:close) do mask = create_mask_key() {:ok, <<1::1, 0::3, 8::4, 1::1, 0::7, mask::bytes-size(4)>>} end + def encode_frame({:close, close_code, <>}) do mask = create_mask_key() payload = <> @@ -250,41 +316,53 @@ defmodule WebSockex.Frame do masked_payload = mask(mask, payload) {:ok, <<1::1, 0::3, 8::4, 1::1, len::7, mask::bytes-size(4), masked_payload::binary>>} end + # Encode Text and Binary frames for {key, opcode} <- Map.take(@opcodes, [:text, :binary]) do def encode_frame({unquote(key), payload}) do mask = create_mask_key() {payload_len_bin, payload_len_size} = get_payload_length_bin(payload) masked_payload = mask(mask, payload) - {:ok, <<1::1, 0::3, unquote(opcode)::4, 1::1, payload_len_bin::bits-size(payload_len_size), mask::bytes-size(4), masked_payload::binary>>} + + {:ok, + <<1::1, 0::3, unquote(opcode)::4, 1::1, payload_len_bin::bits-size(payload_len_size), + mask::bytes-size(4), masked_payload::binary>>} end + # Start Fragments! def encode_frame({:fragment, unquote(key), payload}) do mask = create_mask_key() {payload_len_bin, payload_len_size} = get_payload_length_bin(payload) masked_payload = mask(mask, payload) - {:ok, <<0::1, 0::3, unquote(opcode)::4, 1::1, payload_len_bin::bits-size(payload_len_size), mask::bytes-size(4), masked_payload::binary>>} + + {:ok, + <<0::1, 0::3, unquote(opcode)::4, 1::1, payload_len_bin::bits-size(payload_len_size), + mask::bytes-size(4), masked_payload::binary>>} end end + # Handle other Fragments for {key, fin_bit} <- [{:continuation, 0}, {:finish, 1}] do def encode_frame({unquote(key), payload}) do mask = create_mask_key() {payload_len_bin, payload_len_size} = get_payload_length_bin(payload) masked_payload = mask(mask, payload) - {:ok, <>} + + {:ok, + <>} end end + def encode_frame(frame), do: {:error, %WebSockex.InvalidFrameError{frame: frame}} defp parse_text_payload(len, remaining, buffer) do <> = remaining + if String.valid?(payload) do {:ok, {:text, payload}, rest} else - {:error, %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :text, - buffer: buffer}} + {:error, %WebSockex.FrameError{reason: :invalid_utf8, opcode: :text, buffer: buffer}} end end @@ -294,21 +372,30 @@ defmodule WebSockex.Frame do defp get_payload_length_bin(payload) do case byte_size(payload) do - size when size <= 125 -> {<>, 7} - size when size <= 0xFFFF -> {<<126::7, size::16>>, 16+7} - size when size <= 0x7FFFFFFFFFFFFFFF -> {<<127::7, 0::1, size::63>>, 64+7} - _ -> raise "WTF, Seriously? You're trying to send a payload larger than #{0x7FFFFFFFFFFFFFFF} bytes?" + size when size <= 125 -> + {<>, 7} + + size when size <= 0xFFFF -> + {<<126::7, size::16>>, 16 + 7} + + size when size <= 0x7FFFFFFFFFFFFFFF -> + {<<127::7, 0::1, size::63>>, 64 + 7} + + _ -> + raise "WTF, Seriously? You're trying to send a payload larger than #{0x7FFFFFFFFFFFFFFF} bytes?" end end defp mask(key, payload, acc \\ <<>>) defp mask(_, <<>>, acc), do: acc + for x <- 1..3 do defp mask(<>, <>, acc) do masked = part ^^^ key <> end end + defp mask(<> = key_bin, <>, acc) do masked = part ^^^ key mask(key_bin, rest, <>) diff --git a/lib/websockex/utils.ex b/lib/websockex/utils.ex index 701f860..d8bfeb1 100644 --- a/lib/websockex/utils.ex +++ b/lib/websockex/utils.ex @@ -9,9 +9,11 @@ defmodule WebSockex.Utils do case whereis(name) do nil -> do_spawn(link, [self(), name, conn, module, state, opts]) + pid -> {:error, {:already_started, pid}} end + nil -> do_spawn(link, [self(), conn, module, state, opts]) end @@ -20,6 +22,7 @@ defmodule WebSockex.Utils do defp do_spawn(:link, args) do :proc_lib.start_link(WebSockex, :init, args) end + defp do_spawn(:no_link, args) do :proc_lib.start(WebSockex, :init, args) end @@ -27,12 +30,14 @@ defmodule WebSockex.Utils do # Named Processes def register({:global, name}), do: register({:via, :global, name}) + def register({:via, mod, name} = where_tup) do case mod.register_name(name, self()) do :yes -> true :no -> whereis(where_tup) end end + def register(name) do try do Process.register(self(), name) @@ -43,100 +48,113 @@ defmodule WebSockex.Utils do end def send({:global, name}, msg), do: WebSockex.Utils.send({:via, :global, name}, msg) + def send({:via, mod, name}, msg) do mod.send(name, msg) end + def send(dest, msg) do Kernel.send(dest, msg) end def whereis({:global, name}), do: whereis({:via, :global, name}) + def whereis({:via, mod, name}) do case mod.whereis_name(name) do :undefined -> nil other -> other end end + def whereis(name), do: Process.whereis(name) # Debugging def sys_debug([], _, _), do: [] + def sys_debug(debug, event, state) do :sys.handle_debug(debug, &print_event/3, state, event) end defp print_event(io_dev, {:in, :frame, frame}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} received frame: #{inspect frame}") + IO.puts(io_dev, "*DBG* #{inspect(name)} received frame: #{inspect(frame)}") end + defp print_event(io_dev, {:in, :completed_fragment, frame}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} completed fragmented frame: #{inspect frame}") + IO.puts(io_dev, "*DBG* #{inspect(name)} completed fragmented frame: #{inspect(frame)}") end + defp print_event(io_dev, {:in, :cast, msg}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} received cast msg: #{inspect msg}") + IO.puts(io_dev, "*DBG* #{inspect(name)} received cast msg: #{inspect(msg)}") end + defp print_event(io_dev, {:in, :msg, msg}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} received msg: #{inspect msg}") + IO.puts(io_dev, "*DBG* #{inspect(name)} received msg: #{inspect(msg)}") end + defp print_event(io_dev, {:reply, func, frame}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} replying from #{inspect func} with #{inspect frame}") + IO.puts( + io_dev, + "*DBG* #{inspect(name)} replying from #{inspect(func)} with #{inspect(frame)}" + ) end + defp print_event(io_dev, {:close, :remote, :unexpected}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} had the connection closed unexpectedly by the remote server") + IO.puts( + io_dev, + "*DBG* #{inspect(name)} had the connection closed unexpectedly by the remote server" + ) end + defp print_event(io_dev, {:close, :remote, reason}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} closing with the remote reason: #{inspect reason}") + IO.puts(io_dev, "*DBG* #{inspect(name)} closing with the remote reason: #{inspect(reason)}") end + defp print_event(io_dev, {:close, :local, reason}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} closing with local reason: #{inspect reason}") + IO.puts(io_dev, "*DBG* #{inspect(name)} closing with local reason: #{inspect(reason)}") end + defp print_event(io_dev, {:close, :error, error}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} closing due to error: #{inspect error}") + IO.puts(io_dev, "*DBG* #{inspect(name)} closing due to error: #{inspect(error)}") end + defp print_event(io_dev, :closed, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} closed the connection sucessfully") + IO.puts(io_dev, "*DBG* #{inspect(name)} closed the connection sucessfully") end + defp print_event(io_dev, :timeout_closed, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} forcefully closed the connection because the server was taking too long close") + IO.puts( + io_dev, + "*DBG* #{inspect(name)} forcefully closed the connection because the server was taking too long close" + ) end + defp print_event(io_dev, {:socket_out, :close, :error}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} sending error close frame: {:close, 1011, \"\"}") + IO.puts(io_dev, "*DBG* #{inspect(name)} sending error close frame: {:close, 1011, \"\"}") end + defp print_event(io_dev, {:socket_out, :close, frame}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} sending close frame: #{inspect frame}") + IO.puts(io_dev, "*DBG* #{inspect(name)} sending close frame: #{inspect(frame)}") end + defp print_event(io_dev, {:socket_out, :sync_send, frame}, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} sending frame: #{inspect frame}") + IO.puts(io_dev, "*DBG* #{inspect(name)} sending frame: #{inspect(frame)}") end + defp print_event(io_dev, :reconnect, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} attempting to reconnect") + IO.puts(io_dev, "*DBG* #{inspect(name)} attempting to reconnect") end + defp print_event(io_dev, :connect, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} attempting to connect") + IO.puts(io_dev, "*DBG* #{inspect(name)} attempting to connect") end + defp print_event(io_dev, :connected, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} sucessfully connected") + IO.puts(io_dev, "*DBG* #{inspect(name)} sucessfully connected") end + defp print_event(io_dev, :reconnected, %{name: name}) do - IO.puts(io_dev, - "*DBG* #{inspect name} sucessfully reconnected") + IO.puts(io_dev, "*DBG* #{inspect(name)} sucessfully reconnected") end def parse_debug_options(name, options) do @@ -145,12 +163,13 @@ defmodule WebSockex.Utils do try do :sys.debug_options(opts) catch - _,_ -> - :error_logger.format('~p: ignoring bad debug options ~p~n', - [name, opts]) + _, _ -> + :error_logger.format('~p: ignoring bad debug options ~p~n', [name, opts]) [] end - _ -> [] + + _ -> + [] end end end diff --git a/mix.exs b/mix.exs index 96a61e7..ab69f66 100644 --- a/mix.exs +++ b/mix.exs @@ -2,38 +2,43 @@ defmodule WebSockex.Mixfile do use Mix.Project def project do - [app: :websockex, - name: "WebSockex", - version: "0.4.1", - elixir: "~> 1.3", - description: "An Elixir WebSocket client", - source_url: "https://github.com/Azolo/websockex", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - elixirc_paths: elixirc_paths(Mix.env), - package: package(), - deps: deps(), - docs: docs()] + [ + app: :websockex, + name: "WebSockex", + version: "0.4.1", + elixir: "~> 1.3", + description: "An Elixir WebSocket client", + source_url: "https://github.com/Azolo/websockex", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env()), + package: package(), + deps: deps(), + docs: docs() + ] end defp elixirc_paths(:test), do: ['lib', 'test/support'] defp elixirc_paths(_), do: ['lib'] def application do - [applications: [:logger, :ssl, :crypto], - mod: {WebSockex.Application, []}] + [applications: [:logger, :ssl, :crypto], mod: {WebSockex.Application, []}] end defp deps do - [{:ex_doc, "~> 0.14", only: :dev, runtime: false}, - {:cowboy, "~> 1.0.0", only: :test}, - {:plug, "~> 1.0", only: :test}] + [ + {:ex_doc, "~> 0.14", only: :dev, runtime: false}, + {:cowboy, "~> 1.0.0", only: :test}, + {:plug, "~> 1.0", only: :test} + ] end defp package do - %{licenses: ["MIT"], + %{ + licenses: ["MIT"], maintainers: ["Justin Baker"], - links: %{"GitHub" => "https://github.com/Azolo/websockex"}} + links: %{"GitHub" => "https://github.com/Azolo/websockex"} + } end defp docs do diff --git a/test/support/test_server.ex b/test/support/test_server.ex index 314044f..956f5fc 100644 --- a/test/support/test_server.ex +++ b/test/support/test_server.ex @@ -4,10 +4,11 @@ defmodule WebSockex.TestServer do @certfile Path.join([__DIR__, "priv", "websockex.cer"]) @keyfile Path.join([__DIR__, "priv", "websockex.key"]) - @cacert Path.join([__DIR__, "priv", "websockexca.cer"]) |> File.read! |> :public_key.pem_decode + @cacert Path.join([__DIR__, "priv", "websockexca.cer"]) |> File.read!() + |> :public_key.pem_decode() - plug :match - plug :dispatch + plug(:match) + plug(:dispatch) match _ do send_resp(conn, 200, "Hello from plug") @@ -19,13 +20,12 @@ defmodule WebSockex.TestServer do {:ok, agent_pid} = Agent.start_link(fn -> :ok end) url = "ws://localhost:#{port}/ws" - opts = [dispatch: dispatch({pid, agent_pid}), - port: port, - ref: ref] + opts = [dispatch: dispatch({pid, agent_pid}), port: port, ref: ref] case Plug.Adapters.Cowboy.http(__MODULE__, [], opts) do {:ok, _} -> {:ok, {ref, url}} + {:error, :eaddrinuse} -> start(pid) end @@ -37,18 +37,21 @@ defmodule WebSockex.TestServer do url = "wss://localhost:#{port}/ws" {:ok, agent_pid} = Agent.start_link(fn -> :ok end) - opts = [dispatch: dispatch({pid, agent_pid}), - certfile: @certfile, - keyfile: @keyfile, - port: port, - ref: ref] + opts = [ + dispatch: dispatch({pid, agent_pid}), + certfile: @certfile, + keyfile: @keyfile, + port: port, + ref: ref + ] case Plug.Adapters.Cowboy.https(__MODULE__, [], opts) do {:ok, _} -> {:ok, {ref, url}} + {:error, :eaddrinuse} -> require Logger - Logger.error "Address #{port} in use!" + Logger.error("Address #{port} in use!") start_https(pid) end end @@ -77,7 +80,7 @@ defmodule WebSockex.TestServer do defp get_port do unless Process.whereis(__MODULE__), do: start_ports_agent() - Agent.get_and_update(__MODULE__, fn(port) -> {port, port + 1} end) + Agent.get_and_update(__MODULE__, fn port -> {port, port + 1} end) end defp start_ports_agent do @@ -90,22 +93,27 @@ defmodule WebSockex.TestSocket do def init(_, req, [{test_pid, agent_pid}]) do case Agent.get(agent_pid, fn x -> x end) do - :ok -> {:upgrade, :protocol, :cowboy_websocket} + :ok -> + {:upgrade, :protocol, :cowboy_websocket} + int when is_integer(int) -> :cowboy_req.reply(int, req) {:shutdown, req, :tests_are_fun} + :connection_wait -> send(test_pid, self()) + receive do :connection_continue -> {:upgrade, :protocol, :cowboy_websocket} end + :immediate_reply -> immediate_reply(req) end end - def terminate(_,_,_), do: :ok + def terminate(_, _, _), do: :ok def websocket_init(_, req, [{test_pid, agent_pid}]) do send(test_pid, self()) @@ -115,9 +123,11 @@ defmodule WebSockex.TestSocket do def websocket_terminate({:remote, :closed}, _, state) do send(state.pid, :normal_remote_closed) end + def websocket_terminate({:remote, close_code, reason}, _, state) do send(state.pid, {close_code, reason}) end + def websocket_terminate(_, _, _) do :ok end @@ -126,12 +136,16 @@ defmodule WebSockex.TestSocket do send(state.pid, :erlang.binary_to_term(msg)) {:ok, req, state} end + def websocket_handle({:ping, _}, req, state), do: {:ok, req, state} + def websocket_handle({:pong, ""}, req, state) do send(state.pid, :received_pong) {:ok, req, state} end - def websocket_handle({:pong, payload}, req, %{ping_payload: ping_payload} = state) when payload == ping_payload do + + def websocket_handle({:pong, payload}, req, %{ping_payload: ping_payload} = state) + when payload == ping_payload do send(state.pid, :received_payload_pong) {:ok, req, state} end @@ -139,33 +153,43 @@ defmodule WebSockex.TestSocket do def websocket_info(:stall, _, _) do Process.sleep(:infinity) end + def websocket_info(:send_ping, req, state), do: {:reply, :ping, req, state} + def websocket_info(:send_payload_ping, req, state) do payload = "Llama and Lambs" {:reply, {:ping, payload}, req, Map.put(state, :ping_payload, payload)} end + def websocket_info(:close, req, state), do: {:reply, :close, req, state} + def websocket_info({:close, code, reason}, req, state) do {:reply, {:close, code, reason}, req, state} end + def websocket_info({:send, frame}, req, state) do {:reply, frame, req, state} end + def websocket_info({:set_code, code}, req, state) do Agent.update(state.agent_pid, fn _ -> code end) {:ok, req, state} end + def websocket_info(:connection_wait, req, state) do Agent.update(state.agent_pid, fn _ -> :connection_wait end) {:ok, req, state} end + def websocket_info(:immediate_reply, req, state) do Agent.update(state.agent_pid, fn _ -> :immediate_reply end) {:ok, req, state} end + def websocket_info(:shutdown, req, state) do {:shutdown, req, state} end + def websocket_info(_, req, state), do: {:ok, req, state} @dialyzer {:nowarn_function, immediate_reply: 1} @@ -174,14 +198,19 @@ defmodule WebSockex.TestSocket do transport = elem(req, 2) {headers, _} = :cowboy_req.headers(req) {_, key} = List.keyfind(headers, "sec-websocket-key", 0) - challenge = :crypto.hash(:sha, key <> "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") |> Base.encode64 - - handshake = ["HTTP/1.1 101 Test Socket Upgrade", - "Connection: Upgrade", - "Upgrade: websocket", - "Sec-WebSocket-Accept: #{challenge}", - "\r\n"] |> Enum.join("\r\n") + challenge = + :crypto.hash(:sha, key <> "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") |> Base.encode64() + + handshake = + [ + "HTTP/1.1 101 Test Socket Upgrade", + "Connection: Upgrade", + "Upgrade: websocket", + "Sec-WebSocket-Accept: #{challenge}", + "\r\n" + ] + |> Enum.join("\r\n") frame = <<1::1, 0::3, 1::4, 0::1, 15::7, "Immediate Reply">> transport.send(socket, handshake) diff --git a/test/websockex/conn_test.exs b/test/websockex/conn_test.exs index bc005fd..f6440e5 100644 --- a/test/websockex/conn_test.exs +++ b/test/websockex/conn_test.exs @@ -4,7 +4,7 @@ defmodule WebSockex.ConnTest do setup do {:ok, {server_ref, url}} = WebSockex.TestServer.start(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) uri = URI.parse(url) @@ -26,7 +26,8 @@ defmodule WebSockex.ConnTest do extra_headers: [{"Pineapple", "Cake"}], socket: nil, socket_connect_timeout: 6000, - socket_recv_timeout: 5000} + socket_recv_timeout: 5000 + } ssl_conn = %WebSockex.Conn{ host: "localhost", @@ -38,18 +39,26 @@ defmodule WebSockex.ConnTest do extra_headers: [{"Pineapple", "Cake"}], socket: nil, socket_connect_timeout: 6000, - socket_recv_timeout: 5000} + socket_recv_timeout: 5000 + } regular_url = "ws://localhost/ws" regular_uri = URI.parse(regular_url) + regular_opts = [ extra_headers: [{"Pineapple", "Cake"}], socket_connect_timeout: 123, - socket_recv_timeout: 456\ + socket_recv_timeout: 456 ] - assert WebSockex.Conn.new(regular_uri, regular_opts) == %{tcp_conn | socket_connect_timeout: 123, socket_recv_timeout: 456} - assert WebSockex.Conn.new(regular_url, regular_opts) == WebSockex.Conn.new(regular_uri, regular_opts) + assert WebSockex.Conn.new(regular_uri, regular_opts) == %{ + tcp_conn + | socket_connect_timeout: 123, + socket_recv_timeout: 456 + } + + assert WebSockex.Conn.new(regular_url, regular_opts) == + WebSockex.Conn.new(regular_uri, regular_opts) conn_opts = [extra_headers: [{"Pineapple", "Cake"}]] @@ -70,23 +79,27 @@ defmodule WebSockex.ConnTest do llama_url = "llama://localhost/ws" llama_conn = URI.parse(llama_url) + assert WebSockex.Conn.new(llama_conn, conn_opts) == - %WebSockex.Conn{host: "localhost", - port: nil, - path: "/ws", - query: nil, - conn_mod: nil, - transport: nil, - extra_headers: [{"Pineapple", "Cake"}], - socket: nil, - socket_connect_timeout: 6000, - socket_recv_timeout: 5000} + %WebSockex.Conn{ + host: "localhost", + port: nil, + path: "/ws", + query: nil, + conn_mod: nil, + transport: nil, + extra_headers: [{"Pineapple", "Cake"}], + socket: nil, + socket_connect_timeout: 6000, + socket_recv_timeout: 5000 + } + assert {:error, %WebSockex.URLError{}} = WebSockex.Conn.new(llama_url, conn_opts) end test "parse_url" do assert WebSockex.Conn.parse_url("lemon_pie") == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + {:error, %WebSockex.URLError{url: "lemon_pie"}} ws_url = "ws://localhost/ws" assert WebSockex.Conn.parse_url(ws_url) == {:ok, URI.parse(ws_url)} @@ -107,9 +120,8 @@ defmodule WebSockex.ConnTest do test "open_socket", context do %{host: host, port: port, path: path} = context.uri - assert {:ok, - %WebSockex.Conn{host: ^host, port: ^port, path: ^path, socket: _}} = - WebSockex.Conn.open_socket(context.conn) + assert {:ok, %WebSockex.Conn{host: ^host, port: ^port, path: ^path, socket: _}} = + WebSockex.Conn.open_socket(context.conn) end test "open_socket with bad path", context do @@ -120,35 +132,39 @@ defmodule WebSockex.ConnTest do :ok = WebSockex.Conn.socket_send(conn, request) assert WebSockex.Conn.handle_response(conn) == - {:error, %WebSockex.RequestError{code: 400, message: "Bad Request"}} + {:error, %WebSockex.RequestError{code: 400, message: "Bad Request"}} end describe "secure connection" do setup do {:ok, {server_ref, url}} = WebSockex.TestServer.start_https(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) uri = URI.parse(url) - {:ok, conn} = WebSockex.Conn.new(uri) |> WebSockex.Conn.open_socket + {:ok, conn} = WebSockex.Conn.new(uri) |> WebSockex.Conn.open_socket() [url: url, uri: uri, conn: conn] end test "open_socket with supplied cacerts", context do - conn = WebSockex.Conn.new(context.uri, [insecure: false, - cacerts: WebSockex.TestServer.cacerts()]) + conn = + WebSockex.Conn.new( + context.uri, + insecure: false, + cacerts: WebSockex.TestServer.cacerts() + ) assert {:ok, %WebSockex.Conn{conn_mod: :ssl, transport: :ssl, insecure: false}} = - WebSockex.Conn.open_socket(conn) + WebSockex.Conn.open_socket(conn) end test "open_socket with insecure flag", context do conn = WebSockex.Conn.new(context.uri, insecure: true) assert {:ok, %WebSockex.Conn{conn_mod: :ssl, transport: :ssl, insecure: true}} = - WebSockex.Conn.open_socket(conn) + WebSockex.Conn.open_socket(conn) end test "close_socket", context do @@ -156,7 +172,7 @@ defmodule WebSockex.ConnTest do assert {:ok, _} = :ssl.sockname(socket) assert WebSockex.Conn.close_socket(context.conn) == %{context.conn | socket: nil} - Process.sleep 50 + Process.sleep(50) assert {:error, _} = :ssl.sockname(socket) end end @@ -187,7 +203,7 @@ defmodule WebSockex.ConnTest do test "works on wss connections" do {:ok, {server_ref, url}} = WebSockex.TestServer.start_https(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) uri = URI.parse(url) conn = WebSockex.Conn.new(uri) {:ok, conn} = WebSockex.Conn.open_socket(conn) @@ -209,15 +225,18 @@ defmodule WebSockex.ConnTest do test "socket_send returns a send error when fails to send", %{conn: conn} do socket = conn.socket :ok = conn.conn_mod.close(socket) + assert WebSockex.Conn.socket_send(conn, "Gonna Fail") == - {:error, %WebSockex.ConnError{original: :closed}} + {:error, %WebSockex.ConnError{original: :closed}} end test "build_request" do - conn = %WebSockex.Conn{host: "lime.com", - path: "/coco", - query: "nut=true", - extra_headers: [{"X-Test", "Shoes"}]} + conn = %WebSockex.Conn{ + host: "lime.com", + path: "/coco", + query: "nut=true", + extra_headers: [{"X-Test", "Shoes"}] + } {:ok, request} = WebSockex.Conn.build_request(conn, "pants") diff --git a/test/websockex/frame_test.exs b/test/websockex/frame_test.exs index e5f8844..3c219df 100644 --- a/test/websockex/frame_test.exs +++ b/test/websockex/frame_test.exs @@ -23,12 +23,14 @@ defmodule WebSockex.FrameTest do def unmask(key, payload, acc \\ <<>>) def unmask(_, <<>>, acc), do: acc + for x <- 1..3 do def unmask(<>, <>, acc) do part = payload ^^^ key <> end end + def unmask(<>, <>, acc) do part = payload ^^^ key unmask(<>, rest, <>) @@ -41,20 +43,22 @@ defmodule WebSockex.FrameTest do <> = @ping_frame assert Frame.parse_frame(<>) == :incomplete end + test "handles incomplete frames with complete headers" do frame = <<1::1, 0::3, 1::4, 0::1, 5::7, "Hello"::utf8>> <> = frame assert Frame.parse_frame(part) == :incomplete - assert Frame.parse_frame(<>) == - {:ok, {:text, "Hello"}, <<>>} + assert Frame.parse_frame(<>) == {:ok, {:text, "Hello"}, <<>>} end + test "handles incomplete large frames" do len = 0x5555 frame = <<1::1, 0::3, 1::4, 0::1, 126::7, len::16, 0::500*8, "Hello">> assert Frame.parse_frame(frame) == :incomplete end + test "handles incomplete very large frame" do len = 0x5FFFF frame = <<1::1, 0::3, 1::4, 0::1, 127::7, len::64, 0::1000*8, "Hello">> @@ -64,46 +68,48 @@ defmodule WebSockex.FrameTest do test "returns overflow buffer" do <> = <<@ping_frame, @ping_frame_with_payload>> + payload = <> assert Frame.parse_frame(payload) == {:ok, :ping, overflow} - assert Frame.parse_frame(<>) == - {:ok, {:ping, "Hello"}, <<>>} + assert Frame.parse_frame(<>) == {:ok, {:ping, "Hello"}, <<>>} end test "parses a close frame" do assert Frame.parse_frame(@close_frame) == {:ok, :close, <<>>} end + test "parses a ping frame" do assert Frame.parse_frame(@ping_frame) == {:ok, :ping, <<>>} end + test "parses a pong frame" do assert Frame.parse_frame(@pong_frame) == {:ok, :pong, <<>>} end test "parses a close frame with a payload" do - assert Frame.parse_frame(@close_frame_with_payload) == - {:ok, {:close, 1000, "Hello"}, <<>>} + assert Frame.parse_frame(@close_frame_with_payload) == {:ok, {:close, 1000, "Hello"}, <<>>} end + test "parses a ping frame with a payload" do - assert Frame.parse_frame(@ping_frame_with_payload) == - {:ok, {:ping, "Hello"}, <<>>} + assert Frame.parse_frame(@ping_frame_with_payload) == {:ok, {:ping, "Hello"}, <<>>} end + test "parses a pong frame with a payload" do - assert Frame.parse_frame(@pong_frame_with_payload) == - {:ok, {:pong, "Hello"}, <<>>} + assert Frame.parse_frame(@pong_frame_with_payload) == {:ok, {:pong, "Hello"}, <<>>} end test "parses a text frame" do frame = <<1::1, 0::3, 1::4, 0::1, 5::7, "Hello"::utf8>> - assert Frame.parse_frame(frame) == - {:ok, {:text, "Hello"}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:text, "Hello"}, <<>>} end + test "parses a large text frame" do string = <<0::5000*8, "Hello">> len = byte_size(string) frame = <<1::1, 0::3, 1::4, 0::1, 126::7, len::16, string::binary>> assert Frame.parse_frame(frame) == {:ok, {:text, string}, <<>>} end + test "parses a very large text frame" do string = <<0::80_000*8, "Hello">> len = byte_size(string) @@ -112,157 +118,154 @@ defmodule WebSockex.FrameTest do end test "parses a binary frame" do - len = byte_size @binary + len = byte_size(@binary) frame = <<1::1, 0::3, 2::4, 0::1, len::7, @binary::bytes>> - assert Frame.parse_frame(frame) == - {:ok, {:binary, @binary}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:binary, @binary}, <<>>} end + test "parses a large binary frame" do binary = <<0::5000*8, @binary::binary>> - len = byte_size binary + len = byte_size(binary) frame = <<1::1, 0::3, 2::4, 0::1, 126::7, len::16, binary::binary>> assert Frame.parse_frame(frame) == {:ok, {:binary, binary}, <<>>} end + test "parses a very large binary frame" do binary = <<0::80_000*8, @binary::binary>> - len = byte_size binary + len = byte_size(binary) frame = <<1::1, 0::3, 2::4, 0::1, 127::7, len::64, binary::binary>> assert Frame.parse_frame(frame) == {:ok, {:binary, binary}, <<>>} end test "parses a text fragment frame" do frame = <<0::1, 0::3, 1::4, 0::1, 5::7, "Hello"::utf8>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :text, "Hello"}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :text, "Hello"}, <<>>} end + test "parses a large text fragment frame" do string = <<0::5000*8, "Hello">> len = byte_size(string) frame = <<0::1, 0::3, 1::4, 0::1, 126::7, len::16, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :text, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :text, string}, <<>>} end + test "parses a very large text fragment frame" do string = <<0::80_000*8, "Hello">> len = byte_size(string) frame = <<0::1, 0::3, 1::4, 0::1, 127::7, len::64, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :text, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :text, string}, <<>>} end test "parses a binary fragment frame" do - len = byte_size @binary + len = byte_size(@binary) frame = <<0::1, 0::3, 2::4, 0::1, len::7, @binary::bytes>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :binary, @binary}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :binary, @binary}, <<>>} end + test "parses a large binary fragment frame" do binary = <<0::5000*8, @binary::binary>> - len = byte_size binary + len = byte_size(binary) frame = <<0::1, 0::3, 2::4, 0::1, 126::7, len::16, binary::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :binary, binary}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :binary, binary}, <<>>} end + test "parses a very large binary fragment frame" do binary = <<0::80_000*8, @binary::binary>> - len = byte_size binary + len = byte_size(binary) frame = <<0::1, 0::3, 2::4, 0::1, 127::7, len::64, binary::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:fragment, :binary, binary}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:fragment, :binary, binary}, <<>>} end test "parses a continuation frame in a fragmented segment" do frame = <<0::1, 0::3, 0::4, 0::1, 5::7, "Hello"::utf8>> - assert Frame.parse_frame(frame) == - {:ok, {:continuation, "Hello"}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:continuation, "Hello"}, <<>>} end + test "parses a large continuation frame in a fragmented segment" do string = <<0::5000*8, "Hello">> len = byte_size(string) frame = <<0::1, 0::3, 0::4, 0::1, 126::7, len::16, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:continuation, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:continuation, string}, <<>>} end + test "parses a very large continuation frame in a fragmented segment" do string = <<0::80_000*8, "Hello">> len = byte_size(string) frame = <<0::1, 0::3, 0::4, 0::1, 127::7, len::64, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:continuation, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:continuation, string}, <<>>} end test "parses a finish frame in a fragmented segment" do frame = <<1::1, 0::3, 0::4, 0::1, 5::7, "Hello"::utf8>> - assert Frame.parse_frame(frame) == - {:ok, {:finish, "Hello"}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:finish, "Hello"}, <<>>} end + test "parses a large finish frame in a fragmented segment" do string = <<0::5000*8, "Hello">> len = byte_size(string) frame = <<1::1, 0::3, 0::4, 0::1, 126::7, len::16, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:finish, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:finish, string}, <<>>} end + test "parses a very large finish frame in a fragmented segment" do string = <<0::80_000*8, "Hello">> len = byte_size(string) frame = <<1::1, 0::3, 0::4, 0::1, 127::7, len::64, string::binary>> - assert Frame.parse_frame(frame) == - {:ok, {:finish, string}, <<>>} + assert Frame.parse_frame(frame) == {:ok, {:finish, string}, <<>>} end test "nonfin control frame returns an error" do frame = <<0::1, 0::3, 9::4, 0::1, 0::7>> + assert Frame.parse_frame(frame) == - {:error, - %WebSockex.FrameError{reason: :nonfin_control_frame, - opcode: :ping, - buffer: frame}} + {:error, + %WebSockex.FrameError{reason: :nonfin_control_frame, opcode: :ping, buffer: frame}} end + test "large control frames return an error" do - error = %WebSockex.FrameError{reason: :control_frame_too_large, - opcode: :ping} + error = %WebSockex.FrameError{reason: :control_frame_too_large, opcode: :ping} frame = <<1::1, 0::3, 9::4, 0::1, 126::7>> - assert Frame.parse_frame(frame) == - {:error, %{error | buffer: frame}} + assert Frame.parse_frame(frame) == {:error, %{error | buffer: frame}} frame = <<1::1, 0::3, 9::4, 0::1, 127::7>> - assert Frame.parse_frame(frame) == - {:error, %{error | buffer: frame}} + assert Frame.parse_frame(frame) == {:error, %{error | buffer: frame}} end test "close frames with data must have atleast 2 bytes of data" do frame = <<1::1, 0::3, 8::4, 0::1, 1::7, 0::8>> + assert Frame.parse_frame(frame) == - {:error, - %WebSockex.FrameError{reason: :close_with_single_byte_payload, - opcode: :close, - buffer: frame}} + {:error, + %WebSockex.FrameError{ + reason: :close_with_single_byte_payload, + opcode: :close, + buffer: frame + }} end test "Close Frames with improper close codes return an error" do frame = <<1::1, 0::3, 8::4, 0::1, 7::7, 5000::16, "Hello">> + assert Frame.parse_frame(frame) == - {:error, %WebSockex.FrameError{reason: :invalid_close_code, - opcode: :close, - buffer: frame}} + {:error, + %WebSockex.FrameError{reason: :invalid_close_code, opcode: :close, buffer: frame}} end test "Text Frames check for valid UTF-8" do frame = <<1::1, 0::3, 1::4, 0::1, 7::7, 0xFFFF::16, "Hello"::utf8>> + assert Frame.parse_frame(frame) == - {:error, %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :text, - buffer: frame}} + {:error, + %WebSockex.FrameError{reason: :invalid_utf8, opcode: :text, buffer: frame}} end test "Close Frames with payloads check for valid UTF-8" do frame = <<1::1, 0::3, 8::4, 0::1, 9::7, 1000::16, 0xFFFF::16, "Hello"::utf8>> + assert Frame.parse_frame(frame) == - {:error, %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :close, - buffer: frame}} + {:error, + %WebSockex.FrameError{reason: :invalid_utf8, opcode: :close, buffer: frame}} end end @@ -270,245 +273,337 @@ defmodule WebSockex.FrameTest do test "Errors with two fragment starts" do frame0 = {:fragment, :text, "Hello"} frame1 = {:fragment, :text, "Goodbye"} + assert Frame.parse_fragment(frame0, frame1) == - {:error, - %WebSockex.FragmentParseError{reason: :two_start_frames, - fragment: frame0, - continuation: frame1}} + {:error, + %WebSockex.FragmentParseError{ + reason: :two_start_frames, + fragment: frame0, + continuation: frame1 + }} end test "Applies continuation to a text fragment" do frame = <<0xFFFF::16, "Hello"::utf8>> <> = frame + assert Frame.parse_fragment({:fragment, :text, part}, {:continuation, rest}) == - {:ok, {:fragment, :text, frame}} + {:ok, {:fragment, :text, frame}} end + test "Finishes a text fragment" do frame0 = {:fragment, :text, "Hel"} frame1 = {:finish, "lo"} - assert Frame.parse_fragment(frame0, frame1) == - {:ok, {:text, "Hello"}} + assert Frame.parse_fragment(frame0, frame1) == {:ok, {:text, "Hello"}} end + test "Errors with invalid utf-8 in a text fragment" do frame = <<0xFFFF::16, "Hello"::utf8>> <> = frame + assert Frame.parse_fragment({:fragment, :text, part}, {:finish, rest}) == - {:error, - %WebSockex.FrameError{reason: :invalid_utf8, - opcode: :text, - buffer: frame}} + {:error, + %WebSockex.FrameError{reason: :invalid_utf8, opcode: :text, buffer: frame}} end test "Applies a continuation to a binary fragment" do <> = @binary + assert Frame.parse_fragment({:fragment, :binary, part}, {:continuation, rest}) == - {:ok, {:fragment, :binary, @binary}} + {:ok, {:fragment, :binary, @binary}} end + test "Finishes a binary fragment" do <> = @binary + assert Frame.parse_fragment({:fragment, :binary, part}, {:finish, rest}) == - {:ok, {:binary, @binary}} + {:ok, {:binary, @binary}} end end describe "encode_frame" do test "encodes a ping frame" do - assert {:ok, <<1::1, 0::3, 9::4, 1::1, 0::7, _::32>>} = - Frame.encode_frame(:ping) + assert {:ok, <<1::1, 0::3, 9::4, 1::1, 0::7, _::32>>} = Frame.encode_frame(:ping) end + test "encodes a ping frame with a payload" do payload = "A longer but different string." len = byte_size(payload) - assert {:ok, <<1::1, 0::3, 9::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary-size(len)>>} = - Frame.encode_frame({:ping, payload}) + + assert {:ok, + <<1::1, 0::3, 9::4, 1::1, ^len::7, mask::bytes-size(4), + masked_payload::binary-size(len)>>} = Frame.encode_frame({:ping, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a pong frame" do - assert {:ok, <<1::1, 0::3, 10::4, 1::1, 0::7, _::32>>} = - Frame.encode_frame(:pong) + assert {:ok, <<1::1, 0::3, 10::4, 1::1, 0::7, _::32>>} = Frame.encode_frame(:pong) end + test "encodes a pong frame with a payload" do payload = "No" len = byte_size(payload) - assert {:ok, <<1::1, 0::3, 10::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary-size(len)>>} = - Frame.encode_frame({:pong, payload}) + + assert {:ok, + <<1::1, 0::3, 10::4, 1::1, ^len::7, mask::bytes-size(4), + masked_payload::binary-size(len)>>} = Frame.encode_frame({:pong, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a close frame" do - assert {:ok, <<1::1, 0::3, 8::4, 1::1, 0::7, _::32>>} = - Frame.encode_frame(:close) + assert {:ok, <<1::1, 0::3, 8::4, 1::1, 0::7, _::32>>} = Frame.encode_frame(:close) end + test "encodes a close frame with a payload" do payload = "Hello" len = byte_size(<<1000::16, payload::binary>>) - assert {:ok, <<1::1, 0::3, 8::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary-size(len)>>} = - Frame.encode_frame({:close, 1000, payload}) + + assert {:ok, + <<1::1, 0::3, 8::4, 1::1, ^len::7, mask::bytes-size(4), + masked_payload::binary-size(len)>>} = Frame.encode_frame({:close, 1000, payload}) + assert unmask(mask, masked_payload) == <<1000::16, payload::binary>> end test "returns an error with large ping frame" do assert Frame.encode_frame({:ping, @large_binary}) == - {:error, - %WebSockex.FrameEncodeError{reason: :control_frame_too_large, - frame_type: :ping, - frame_payload: @large_binary}} + {:error, + %WebSockex.FrameEncodeError{ + reason: :control_frame_too_large, + frame_type: :ping, + frame_payload: @large_binary + }} end + test "returns an error with large pong frame" do assert Frame.encode_frame({:pong, @large_binary}) == - {:error, - %WebSockex.FrameEncodeError{reason: :control_frame_too_large, - frame_type: :pong, - frame_payload: @large_binary}} + {:error, + %WebSockex.FrameEncodeError{ + reason: :control_frame_too_large, + frame_type: :pong, + frame_payload: @large_binary + }} end + test "returns an error with large close frame" do assert Frame.encode_frame({:close, 1000, @large_binary}) == - {:error, - %WebSockex.FrameEncodeError{reason: :control_frame_too_large, - frame_type: :close, - frame_payload: @large_binary, - close_code: 1000}} + {:error, + %WebSockex.FrameEncodeError{ + reason: :control_frame_too_large, + frame_type: :close, + frame_payload: @large_binary, + close_code: 1000 + }} end test "returns an error with close code out of range" do assert Frame.encode_frame({:close, 5838, "Hello"}) == - {:error, - %WebSockex.FrameEncodeError{reason: :close_code_out_of_range, - frame_type: :close, - frame_payload: "Hello", - close_code: 5838}} + {:error, + %WebSockex.FrameEncodeError{ + reason: :close_code_out_of_range, + frame_type: :close, + frame_payload: "Hello", + close_code: 5838 + }} end test "encodes a text frame" do payload = "Lemon Pies are Pies." - len = byte_size payload - assert {:ok, <<1::1, 0::3, 1::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:text, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 1::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:text, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large text frame" do payload = <<0::300*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 1::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:text, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 1::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:text, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large text frame" do payload = <<0::0xFFFFF*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 1::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:text, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 1::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:text, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a binary frame" do payload = @binary - len = byte_size payload - assert {:ok, <<1::1, 0::3, 2::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 2::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:binary, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large binary frame" do payload = <<0::300*8, @binary::binary>> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 2::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 2::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:binary, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large binary frame" do payload = <<0::0xFFFFF*8, @binary::binary>> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 2::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 2::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:binary, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a text fragment frame" do payload = "Lemon Pies are Pies." - len = byte_size payload - assert {:ok, <<0::1, 0::3, 1::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :text, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 1::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:fragment, :text, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large text fragment frame" do payload = <<0::300*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 1::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :text, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 1::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:fragment, :text, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large text fragment frame" do payload = <<0::0xFFFFF*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 1::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :text, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 1::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:fragment, :text, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a binary fragment frame" do payload = @binary - len = byte_size payload - assert {:ok, <<0::1, 0::3, 2::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 2::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:fragment, :binary, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large binary fragment frame" do payload = <<0::300*8, @binary::binary>> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 2::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 2::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:fragment, :binary, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large binary fragment frame" do payload = <<0::0xFFFFF*8, @binary::binary>> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 2::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:fragment, :binary, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 2::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:fragment, :binary, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a continuation frame" do payload = "Lemon Pies are Pies." - len = byte_size payload - assert {:ok, <<0::1, 0::3, 0::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:continuation, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 0::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:continuation, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large continuation frame" do payload = <<0::300*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 0::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:continuation, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 0::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:continuation, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large continuation frame" do payload = <<0::0xFFFFF*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<0::1, 0::3, 0::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:continuation, payload}) + len = byte_size(payload) + + assert {:ok, + <<0::1, 0::3, 0::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:continuation, payload}) + assert unmask(mask, masked_payload) == payload end test "encodes a finish to a fragmented segment" do payload = "Lemon Pies are Pies." - len = byte_size payload - assert {:ok, <<1::1, 0::3, 0::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:finish, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 0::4, 1::1, ^len::7, mask::bytes-size(4), masked_payload::binary>>} = + Frame.encode_frame({:finish, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a large finish to a fragmented segment" do payload = <<0::300*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 0::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:finish, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 0::4, 1::1, 126::7, ^len::16, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:finish, payload}) + assert unmask(mask, masked_payload) == payload end + test "encodes a very large finish to a fragmented segment" do payload = <<0::0xFFFFF*8, "Lemon Pies are Pies.">> - len = byte_size payload - assert {:ok, <<1::1, 0::3, 0::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), masked_payload::binary>>} = - Frame.encode_frame({:finish, payload}) + len = byte_size(payload) + + assert {:ok, + <<1::1, 0::3, 0::4, 1::1, 127::7, ^len::64, mask::bytes-size(4), + masked_payload::binary>>} = Frame.encode_frame({:finish, payload}) + assert unmask(mask, masked_payload) == payload end end diff --git a/test/websockex_nonasync_test.exs b/test/websockex_nonasync_test.exs index 049db63..37811c2 100644 --- a/test/websockex_nonasync_test.exs +++ b/test/websockex_nonasync_test.exs @@ -7,14 +7,14 @@ defmodule WebSockexNonasyncTest do test "supplies a ApplicationError when the application is not started" do Application.stop(:websockex) - on_exit fn -> Application.ensure_all_started(:websockex) end + on_exit(fn -> Application.ensure_all_started(:websockex) end) - refute Application.started_applications |> List.keyfind(:websockex, 0) + refute Application.started_applications() |> List.keyfind(:websockex, 0) :ets.delete(:elixir_config, {:uri, "ws"}) refute URI.default_port("ws") assert WebSockex.start_link("ws://fakeurl", DummyClient, %{}) == - {:error, %WebSockex.ApplicationError{reason: :not_started}} + {:error, %WebSockex.ApplicationError{reason: :not_started}} end end diff --git a/test/websockex_test.exs b/test/websockex_test.exs index d29f8ed..8bbfb36 100644 --- a/test/websockex_test.exs +++ b/test/websockex_test.exs @@ -21,6 +21,7 @@ defmodule WebSockexTest do end def catch_attr(client, atom), do: catch_attr(client, atom, self()) + def catch_attr(client, atom, receiver) do attr = "catch_" <> Atom.to_string(atom) WebSockex.cast(client, {:set_attr, String.to_atom(attr), receiver}) @@ -29,9 +30,11 @@ defmodule WebSockexTest do def format_status(_opt, [_pdict, %{fail_status: true}]) do raise "Failed Status" end + def format_status(_opt, [_pdict, %{non_list_status: true}]) do {:data, "Not a list!"} end + def format_status(_opt, [_pdict, %{custom_status: true}]) do [{:data, [{"Lemon", :pies}]}] end @@ -61,22 +64,25 @@ defmodule WebSockexTest do end def handle_connect(_conn, %{connect_badreply: true}), do: :lemons - def handle_connect(_conn, %{connect_error: true}), do: raise "Connect Error" - def handle_connect(_conn, %{connect_exit: true}), do: exit "Connect Exit" + def handle_connect(_conn, %{connect_error: true}), do: raise("Connect Error") + def handle_connect(_conn, %{connect_exit: true}), do: exit("Connect Exit") + def handle_connect(_conn, %{catch_connect: pid} = args) do send(pid, :caught_connect) {:ok, args} end + def handle_connect(_conn, %{async_test: true}) do receive do {:continue_async, pid} -> send(pid, :async_test) - exit "Async Test" - after - 50 -> - raise "Async Timeout" + exit("Async Test") + after + 50 -> + raise "Async Timeout" end end + def handle_connect(conn, state) do {:ok, Map.put(state, :conn, conn)} end @@ -85,18 +91,23 @@ defmodule WebSockexTest do send(pid, :cast) {:ok, state} end + def handle_cast({:set_state, state}, _state), do: {:ok, state} def handle_cast({:set_attr, key, attr}, state), do: {:ok, Map.put(state, key, attr)} + def handle_cast({:get_state, pid}, state) do send(pid, state) {:ok, state} end + def handle_cast({:send, frame}, state), do: {:reply, frame, state} def handle_cast(:close, state), do: {:close, state} def handle_cast({:close, code, reason}, state), do: {:close, {code, reason}, state} + def handle_cast(:self_send, _) do WebSockex.send_frame(self(), :ping) end + def handle_cast(:delayed_close, state) do receive do {:tcp_closed, socket} -> @@ -104,34 +115,40 @@ defmodule WebSockexTest do {:close, state} end end + def handle_cast({:send_conn, pid}, %{conn: conn} = state) do - send pid, conn + send(pid, conn) {:ok, state} end + def handle_cast(:test_reconnect, state) do {:close, {4985, "Testing Reconnect"}, state} end + def handle_cast(:bad_frame, state) do {:reply, {:haha, "No"}, state} end + def handle_cast(:bad_reply, _), do: :lemon_pie - def handle_cast(:error, _), do: raise "Cast Error" - def handle_cast(:exit, _), do: exit "Cast Exit" + def handle_cast(:error, _), do: raise("Cast Error") + def handle_cast(:exit, _), do: exit("Cast Exit") def handle_info({:send, frame}, state), do: {:reply, frame, state} def handle_info(:close, state), do: {:close, state} def handle_info({:close, code, reason}, state), do: {:close, {code, reason}, state} + def handle_info({:pid_reply, pid}, state) do send(pid, :info) {:ok, state} end + def handle_info(:bad_reply, _), do: :lemon_pie - def handle_info(:error, _), do: raise "Info Error" - def handle_info(:exit, _), do: exit "Info Exit" + def handle_info(:error, _), do: raise("Info Error") + def handle_info(:exit, _), do: exit("Info Exit") def handle_ping({:ping, "Bad Reply"}, _), do: :lemon_pie - def handle_ping({:ping, "Error"}, _), do: raise "Ping Error" - def handle_ping({:ping, "Exit"}, _), do: exit "Ping Exit" + def handle_ping({:ping, "Error"}, _), do: raise("Ping Error") + def handle_ping({:ping, "Exit"}, _), do: exit("Ping Exit") def handle_ping({:ping, "Please Reply"}, state), do: {:reply, {:pong, "No"}, state} def handle_ping(frame, state), do: super(frame, state) @@ -140,81 +157,106 @@ defmodule WebSockexTest do send(pid, :caught_pong) super(frame, state) end + def handle_pong({:pong, msg} = frame, %{catch_pong: pid} = state) do send(pid, {:caught_payload_pong, msg}) super(frame, state) end + def handle_pong({:pong, "Bad Reply"}, _), do: :lemon_pie - def handle_pong({:pong, "Error"}, _), do: raise "Pong Error" - def handle_pong({:pong, "Exit"}, _), do: exit "Pong Exit" + def handle_pong({:pong, "Error"}, _), do: raise("Pong Error") + def handle_pong({:pong, "Exit"}, _), do: exit("Pong Exit") def handle_pong({:pong, "Please Reply"}, state), do: {:reply, {:text, "No"}, state} def handle_frame({:binary, msg}, %{catch_binary: pid} = state) do send(pid, {:caught_binary, msg}) {:ok, state} end + def handle_frame({:text, msg}, %{catch_text: pid} = state) do send(pid, {:caught_text, msg}) {:ok, state} end + def handle_frame({:text, "Please Reply"}, state), do: {:reply, {:text, "No"}, state} def handle_frame({:text, "Bad Reply"}, _), do: :lemon_pie - def handle_frame({:text, "Error"}, _), do: raise "Frame Error" - def handle_frame({:text, "Exit"}, _), do: exit "Frame Exit" + def handle_frame({:text, "Error"}, _), do: raise("Frame Error") + def handle_frame({:text, "Exit"}, _), do: exit("Frame Exit") def handle_disconnect(_, %{disconnect_badreply: true}), do: :lemons - def handle_disconnect(_, %{disconnect_error: true}), do: raise "Disconnect Error" - def handle_disconnect(_, %{disconnect_exit: true}), do: exit "Disconnect Exit" + def handle_disconnect(_, %{disconnect_error: true}), do: raise("Disconnect Error") + def handle_disconnect(_, %{disconnect_exit: true}), do: exit("Disconnect Exit") + def handle_disconnect(_, %{catch_init_connect_failure: pid} = state) do send(pid, :caught_initial_conn_failure) {:ok, state} end + def handle_disconnect(%{attempt_number: 3} = failure_map, %{multiple_reconnect: pid} = state) do send(pid, {:stopping_retry, failure_map}) {:ok, state} end - def handle_disconnect(%{attempt_number: attempt} = failure_map, %{multiple_reconnect: pid} = state) do + + def handle_disconnect( + %{attempt_number: attempt} = failure_map, + %{multiple_reconnect: pid} = state + ) do send(pid, {:retry_connect, failure_map}) send(pid, {:check_retry_state, %{attempt: attempt, state: state}}) {:reconnect, Map.put(state, :attempt, attempt)} end + def handle_disconnect(_, %{change_conn_reconnect: pid, good_url: url} = state) do uri = URI.parse(url) conn = WebSockex.Conn.new(uri) send(pid, :retry_change_conn) {:reconnect, conn, state} end + def handle_disconnect(%{reason: {:local, 4985, _}}, state) do {:reconnect, state} end - def handle_disconnect(%{reason: {:remote, :closed}}, %{catch_disconnect: pid, reconnect: true} = state) do - send(pid, {:caught_disconnect, :reconnecting}) - {:reconnect, state} + + def handle_disconnect( + %{reason: {:remote, :closed}}, + %{catch_disconnect: pid, reconnect: true} = state + ) do + send(pid, {:caught_disconnect, :reconnecting}) + {:reconnect, state} end + def handle_disconnect(_, %{reconnect: true} = state) do {:reconnect, state} end - def handle_disconnect(%{reason: {:remote, :closed} = reason} = map, - %{catch_disconnect: pid} = state) do + + def handle_disconnect( + %{reason: {:remote, :closed} = reason} = map, + %{catch_disconnect: pid} = state + ) do send(pid, {:caught_disconnect, reason}) super(map, state) end - def handle_disconnect(%{reason: {_, :normal}} = map, - %{catch_disconnect: pid} = state) do + + def handle_disconnect(%{reason: {_, :normal}} = map, %{catch_disconnect: pid} = state) do send(pid, :caught_disconnect) super(map, state) end + def handle_disconnect(%{reason: {_, code, reason}}, %{catch_disconnect: pid} = state) do send(pid, {:caught_disconnect, code, reason}) {:ok, state} end + def handle_disconnect(_, %{catch_disconnect: pid} = state) do send(pid, :caught_disconnect) {:ok, state} end + def handle_disconnect(_, state), do: {:ok, state} - def terminate({:local, :normal}, %{catch_terminate: pid}), do: send(pid, :normal_close_terminate) + def terminate({:local, :normal}, %{catch_terminate: pid}), + do: send(pid, :normal_close_terminate) + def terminate(_, %{catch_terminate: pid}), do: send(pid, :terminate) def terminate(_, _), do: :ok end @@ -226,10 +268,10 @@ defmodule WebSockexTest do setup do {:ok, {server_ref, url}} = WebSockex.TestServer.start(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) {:ok, pid} = TestClient.start_link(url, %{}) - server_pid = WebSockex.TestServer.receive_socket_pid + server_pid = WebSockex.TestServer.receive_socket_pid() [pid: pid, url: url, server_pid: server_pid, server_ref: server_ref] end @@ -247,13 +289,14 @@ defmodule WebSockexTest do test "errors with an already registered name", context do Process.register(self(), context.name) + assert TestClient.start_link(context.url, %{}, name: context.name) == - {:error, {:already_started, self()}} + {:error, {:already_started, self()}} end test "can receive cast messages", context do {:ok, _} = TestClient.start_link(context.url, %{test: :yep}, name: context.name) - WebSockex.TestServer.receive_socket_pid + WebSockex.TestServer.receive_socket_pid() assert %{test: :yep, conn: _} = TestClient.get_state(context.name) end @@ -272,13 +315,14 @@ defmodule WebSockexTest do test "errors with an already registered name", context do {:ok, pid} = TestClient.start_link(context.url, %{}, name: context.name) + assert TestClient.start_link(context.url, %{}, name: context.name) == - {:error, {:already_started, pid}} + {:error, {:already_started, pid}} end test "can receive cast messages", context do {:ok, _} = TestClient.start_link(context.url, %{test: :yep}, name: context.name) - WebSockex.TestServer.receive_socket_pid + WebSockex.TestServer.receive_socket_pid() assert %{test: :yep, conn: _} = TestClient.get_state(context.name) end @@ -297,13 +341,14 @@ defmodule WebSockexTest do test "errors with an already registered name", context do {:ok, pid} = TestClient.start_link(context.url, %{}, name: context.name) + assert TestClient.start_link(context.url, %{}, name: context.name) == - {:error, {:already_started, pid}} + {:error, {:already_started, pid}} end test "can receive cast messages", context do {:ok, _} = TestClient.start_link(context.url, %{test: :yep}, name: context.name) - WebSockex.TestServer.receive_socket_pid + WebSockex.TestServer.receive_socket_pid() assert %{test: :yep, conn: _} = TestClient.get_state(context.name) end @@ -321,8 +366,7 @@ defmodule WebSockexTest do end test "with async option failure", context do - assert {:ok, pid} = - TestClient.start(context.url, %{async_test: true}, async: true) + assert {:ok, pid} = TestClient.start(context.url, %{async_test: true}, async: true) Process.monitor(pid) @@ -334,13 +378,14 @@ defmodule WebSockexTest do test "without async option", context do Process.flag(:trap_exit, true) + assert TestClient.start(context.url, %{async_test: true}) == - {:error, %RuntimeError{message: "Async Timeout"}} + {:error, %RuntimeError{message: "Async Timeout"}} end test "returns an error with a bad url" do assert TestClient.start_link("lemon_pie", :ok) == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + {:error, %WebSockex.URLError{url: "lemon_pie"}} end end @@ -356,25 +401,24 @@ defmodule WebSockexTest do end test "with async option", context do - assert {:ok, _} = - TestClient.start_link(context.url, %{catch_text: self()}, async: true) + assert {:ok, _} = TestClient.start_link(context.url, %{catch_text: self()}, async: true) server_pid = WebSockex.TestServer.receive_socket_pid() - send server_pid, {:send, {:text, "Hello"}} + send(server_pid, {:send, {:text, "Hello"}}) assert_receive {:caught_text, "Hello"} end test "without async option", context do Process.flag(:trap_exit, true) + assert TestClient.start_link(context.url, %{async_test: true}) == - {:error, %RuntimeError{message: "Async Timeout"}} + {:error, %RuntimeError{message: "Async Timeout"}} end test "with async option failure", context do Process.flag(:trap_exit, true) - assert {:ok, pid} = - TestClient.start_link(context.url, %{async_test: true}, async: true) + assert {:ok, pid} = TestClient.start_link(context.url, %{async_test: true}, async: true) send(pid, {:continue_async, self()}) @@ -384,22 +428,22 @@ defmodule WebSockexTest do test "returns an error with a bad url" do assert TestClient.start_link("lemon_pie", :ok) == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + {:error, %WebSockex.URLError{url: "lemon_pie"}} end end test "can handle initial connect headers" do {:ok, {server_ref, url}} = WebSockex.TestServer.start_https(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) + + {:ok, pid} = TestClient.start_link(url, %{}, cacerts: WebSockex.TestServer.cacerts()) + conn = TestClient.get_conn(pid) - {:ok, pid} = TestClient.start_link(url, %{}, cacerts: WebSockex.TestServer.cacerts) - conn = TestClient.get_conn pid - headers = Enum.into(conn.resp_headers, %{}) - refute is_nil headers[:Connection] - refute is_nil headers[:Upgrade] - refute is_nil headers["Sec-Websocket-Accept"] + refute is_nil(headers[:Connection]) + refute is_nil(headers[:Upgrade]) + refute is_nil(headers["Sec-Websocket-Accept"]) TestClient.catch_attr(pid, :pong, self()) end @@ -407,15 +451,15 @@ defmodule WebSockexTest do test "can connect to secure server" do {:ok, {server_ref, url}} = WebSockex.TestServer.start_https(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) - {:ok, pid} = TestClient.start_link(url, %{}, cacerts: WebSockex.TestServer.cacerts) - server_pid = WebSockex.TestServer.receive_socket_pid + {:ok, pid} = TestClient.start_link(url, %{}, cacerts: WebSockex.TestServer.cacerts()) + server_pid = WebSockex.TestServer.receive_socket_pid() TestClient.catch_attr(pid, :pong, self()) # Test server -> client ping - send server_pid, :send_ping + send(server_pid, :send_ping) assert_receive :received_pong # Test client -> server ping @@ -434,10 +478,10 @@ defmodule WebSockexTest do test "handles a ssl message send right after connecting" do {:ok, {server_ref, url}} = WebSockex.TestServer.start_https(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) {:ok, _pid} = TestClient.start_link(url, %{}) - server_pid = WebSockex.TestServer.receive_socket_pid + server_pid = WebSockex.TestServer.receive_socket_pid() send(server_pid, :immediate_reply) @@ -476,8 +520,8 @@ defmodule WebSockexTest do assert_receive conn = %WebSockex.Conn{} :inet.setopts(conn.socket, active: false) - send context.server_pid, {:send, {:text, "Hello"}} - send context.server_pid, {:send, {:text, "Bye"}} + send(context.server_pid, {:send, {:text, "Hello"}}) + send(context.server_pid, {:send, {:text, "Bye"}}) :inet.setopts(conn.socket, active: true) @@ -561,9 +605,11 @@ defmodule WebSockexTest do # Really glad that I got those sys behaviors now :sys.suspend(pid) - task = Task.async(fn -> - WebSockex.send_frame(pid, {:text, "hello"}) - end) + task = + Task.async(fn -> + WebSockex.send_frame(pid, {:text, "hello"}) + end) + :gen_tcp.shutdown(conn.socket, :write) :sys.resume(pid) @@ -585,7 +631,7 @@ defmodule WebSockexTest do WebSockex.TestServer.receive_socket_pid() assert WebSockex.send_frame(context.pid, {:text, "Test"}) == - {:error, %WebSockex.NotConnectedError{connection_state: :opening}} + {:error, %WebSockex.NotConnectedError{connection_state: :opening}} end test "returns a descriptive error message for trying to send a frame from self", context do @@ -604,7 +650,8 @@ defmodule WebSockexTest do WebSockex.cast(pid, :close) assert WebSockex.send_frame(pid, {:text, "Test"}) == - {:error, %WebSockex.NotConnectedError{connection_state: :closing}} + {:error, %WebSockex.NotConnectedError{connection_state: :closing}} + refute_receive {:EXIT, ^pid, _} end end @@ -822,7 +869,7 @@ defmodule WebSockexTest do test "executes in handle_frame bad reply", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:text, "Bad Reply"}} + send(context.server_pid, {:send, {:text, "Bad Reply"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, %WebSockex.BadResponseError{}} @@ -831,7 +878,7 @@ defmodule WebSockexTest do test "executes in handle_frame error", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:text, "Error"}} + send(context.server_pid, {:send, {:text, "Error"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, {%RuntimeError{message: "Frame Error"}, _}} @@ -840,7 +887,7 @@ defmodule WebSockexTest do test "executes in handle_frame exit", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:text, "Exit"}} + send(context.server_pid, {:send, {:text, "Exit"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, "Frame Exit"} @@ -849,7 +896,7 @@ defmodule WebSockexTest do test "executes in handle_ping bad reply", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:ping, "Bad Reply"}} + send(context.server_pid, {:send, {:ping, "Bad Reply"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, %WebSockex.BadResponseError{}}, 500 @@ -858,7 +905,7 @@ defmodule WebSockexTest do test "executes in handle_ping error", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:ping, "Error"}} + send(context.server_pid, {:send, {:ping, "Error"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, {%RuntimeError{message: "Ping Error"}, _}} @@ -867,7 +914,7 @@ defmodule WebSockexTest do test "executes in handle_ping exit", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:ping, "Exit"}} + send(context.server_pid, {:send, {:ping, "Exit"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, "Ping Exit"} @@ -876,7 +923,7 @@ defmodule WebSockexTest do test "executes in handle_pong bad reply", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:pong, "Bad Reply"}} + send(context.server_pid, {:send, {:pong, "Bad Reply"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, %WebSockex.BadResponseError{}} @@ -885,7 +932,7 @@ defmodule WebSockexTest do test "executes in handle_pong error", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:pong, "Error"}} + send(context.server_pid, {:send, {:pong, "Error"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, {%RuntimeError{message: "Pong Error"}, _}} @@ -894,7 +941,7 @@ defmodule WebSockexTest do test "executes in handle_pong exit", %{pid: pid} = context do Process.flag(:trap_exit, true) - send context.server_pid, {:send, {:pong, "Exit"}} + send(context.server_pid, {:send, {:pong, "Exit"}}) assert_receive {1011, ""} assert_receive {:EXIT, ^pid, "Pong Exit"} @@ -933,9 +980,11 @@ defmodule WebSockexTest do test "is not executed in handle_disconnect before initialized", context do assert {:error, %WebSockex.BadResponseError{}} = - TestClient.start_link(context.url <> "bad", - %{disconnect_badreply: true}, - handle_initial_conn_failure: true) + TestClient.start_link( + context.url <> "bad", + %{disconnect_badreply: true}, + handle_initial_conn_failure: true + ) refute_received :terminate end @@ -975,9 +1024,11 @@ defmodule WebSockexTest do test "is not executed in handle_connect before initialized", context do assert {:error, %WebSockex.BadResponseError{}} = - TestClient.start_link(context.url, - %{connect_badreply: true}, - handle_initial_conn_failure: true) + TestClient.start_link( + context.url, + %{connect_badreply: true}, + handle_initial_conn_failure: true + ) refute_received :terminate end @@ -991,6 +1042,7 @@ defmodule WebSockexTest do test "does not catch exits", %{pid: orig_pid} = context do defmodule TerminateClient do use WebSockex + def start(url, state, opts \\ []) do WebSockex.start(url, __MODULE__, state, opts) end @@ -1131,7 +1183,7 @@ defmodule WebSockexTest do assert_receive {4985, "Testing Reconnect"} - server_pid = WebSockex.TestServer.receive_socket_pid + server_pid = WebSockex.TestServer.receive_socket_pid() send(server_pid, {:send, {:text, "Hello"}}) assert_receive {:caught_text, "Hello"}, 500 @@ -1145,15 +1197,21 @@ defmodule WebSockexTest do assert_receive {:retry_connect, %{conn: %WebSockex.Conn{}, attempt_number: 1}} assert_receive {:check_retry_state, %{attempt: 1}} - assert_receive {:retry_connect, %{conn: %WebSockex.Conn{}, reason: %{code: 403}, attempt_number: 2}} + + assert_receive {:retry_connect, + %{conn: %WebSockex.Conn{}, reason: %{code: 403}, attempt_number: 2}} + assert_receive {:check_retry_state, %{attempt: 2, state: %{attempt: 1}}} - assert_receive {:stopping_retry, %{conn: %WebSockex.Conn{}, reason: %{code: 403}, attempt_number: 3}} + + assert_receive {:stopping_retry, + %{conn: %WebSockex.Conn{}, reason: %{code: 403}, attempt_number: 3}} + assert_receive {:DOWN, _ref, :process, ^client_pid, %WebSockex.RequestError{code: 403}} end test "can provide new conn struct during reconnect", context do {:ok, {server_ref, new_url}} = WebSockex.TestServer.start(self()) - on_exit fn -> WebSockex.TestServer.shutdown(server_ref) end + on_exit(fn -> WebSockex.TestServer.shutdown(server_ref) end) WebSockex.cast(context.pid, {:set_attr, :change_conn_reconnect, self()}) WebSockex.cast(context.pid, {:set_attr, :good_url, new_url}) @@ -1208,7 +1266,7 @@ defmodule WebSockexTest do assert_receive {:caught_disconnect, :reconnecting} - server_pid = WebSockex.TestServer.receive_socket_pid + server_pid = WebSockex.TestServer.receive_socket_pid() send(server_pid, {:send, {:text, "Hello"}}) assert_receive {:caught_text, "Hello"} @@ -1225,34 +1283,55 @@ defmodule WebSockexTest do end test "gets invoked during initial connect with handle_initial_conn_failure", context do - assert {:error, _} = TestClient.start_link(context.url <> "bad", - %{catch_init_connect_failure: self()}, - handle_initial_conn_failure: true) + assert {:error, _} = + TestClient.start_link( + context.url <> "bad", + %{catch_init_connect_failure: self()}, + handle_initial_conn_failure: true + ) assert_receive :caught_initial_conn_failure end test "doesn't get invoked during initial connect without retry", context do - assert {:error, _} = TestClient.start_link(context.url <> "bad", %{catch_init_connect_failure: self()}) + assert {:error, _} = + TestClient.start_link(context.url <> "bad", %{catch_init_connect_failure: self()}) refute_receive :caught_initial_conn_failure end test "can attempt to reconnect during an initial connect", context do - assert {:error, _} = TestClient.start_link(context.url <> "bad", - %{multiple_reconnect: self()}, - handle_initial_conn_failure: true) + assert {:error, _} = + TestClient.start_link( + context.url <> "bad", + %{multiple_reconnect: self()}, + handle_initial_conn_failure: true + ) + + assert_received {:retry_connect, + %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 1}} - assert_received {:retry_connect, %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 1}} assert_received {:check_retry_state, %{attempt: 1}} - assert_received {:retry_connect, %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 2}} + + assert_received {:retry_connect, + %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 2}} + assert_received {:check_retry_state, %{attempt: 2, state: %{attempt: 1}}} - assert_received {:stopping_retry, %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 3}} + + assert_received {:stopping_retry, + %{conn: %WebSockex.Conn{}, reason: %{code: 404}, attempt_number: 3}} end test "can reconnect with a new conn struct during an initial connection retry", context do state_map = %{change_conn_reconnect: self(), good_url: context.url, catch_text: self()} - assert {:ok, _} = TestClient.start_link(context.url <> "bad", state_map, handle_initial_conn_failure: true) + + assert {:ok, _} = + TestClient.start_link( + context.url <> "bad", + state_map, + handle_initial_conn_failure: true + ) + server_pid = WebSockex.TestServer.receive_socket_pid() assert_received :retry_change_conn @@ -1271,22 +1350,24 @@ defmodule WebSockexTest do {:ok, pid} = WebSockex.start_link(context.url, BareClient, :test) refute capture_log(fn -> - {{:data, data}, _} = elem(:sys.get_status(pid), 3) - |> List.last - |> List.keydelete(:data, 0) - |> List.keytake(:data, 0) + {{:data, data}, _} = + elem(:sys.get_status(pid), 3) + |> List.last() + |> List.keydelete(:data, 0) + |> List.keytake(:data, 0) - assert [{"State", _}] = data - end) =~ "There was an error while invoking #{__MODULE__}.TestClient.format_status/2" + assert [{"State", _}] = data + end) =~ "There was an error while invoking #{__MODULE__}.TestClient.format_status/2" end test "is invoked when implemented", context do WebSockex.cast(context.pid, {:set_attr, :custom_status, true}) - {{:data, data}, _} = elem(:sys.get_status(context.pid), 3) - |> List.last - |> List.keydelete(:data, 0) - |> List.keytake(:data, 0) + {{:data, data}, _} = + elem(:sys.get_status(context.pid), 3) + |> List.last() + |> List.keydelete(:data, 0) + |> List.keytake(:data, 0) assert [{"Lemon", :pies}] = data end @@ -1296,22 +1377,24 @@ defmodule WebSockexTest do WebSockex.cast(context.pid, {:set_attr, :fail_status, true}) assert capture_log(fn -> - {{:data, data}, _} = elem(:sys.get_status(context.pid), 3) - |> List.last - |> List.keydelete(:data, 0) - |> List.keytake(:data, 0) + {{:data, data}, _} = + elem(:sys.get_status(context.pid), 3) + |> List.last() + |> List.keydelete(:data, 0) + |> List.keytake(:data, 0) - assert [{"State", _}] = data - end) =~ "There was an error while invoking #{__MODULE__}.TestClient.format_status/2" + assert [{"State", _}] = data + end) =~ "There was an error while invoking #{__MODULE__}.TestClient.format_status/2" end test "wraps a non-list return", context do WebSockex.cast(context.pid, {:set_attr, :non_list_status, true}) - {{:data, data}, _} = elem(:sys.get_status(context.pid), 3) - |> List.last - |> List.keydelete(:data, 0) - |> List.keytake(:data, 0) + {{:data, data}, _} = + elem(:sys.get_status(context.pid), 3) + |> List.last() + |> List.keydelete(:data, 0) + |> List.keytake(:data, 0) assert data == "Not a list!" end @@ -1319,13 +1402,13 @@ defmodule WebSockexTest do test "Won't exit on a request error", context do assert TestClient.start_link(context.url <> "blah", %{}) == - {:error, %WebSockex.RequestError{code: 404, message: "Not Found"}} + {:error, %WebSockex.RequestError{code: 404, message: "Not Found"}} end describe "default implementation errors" do setup context do {:ok, pid} = WebSockex.start_link(context.url, BareClient, %{}) - server_pid = WebSockex.TestServer.receive_socket_pid + server_pid = WebSockex.TestServer.receive_socket_pid() [pid: pid, server_pid: server_pid] end @@ -1335,7 +1418,9 @@ defmodule WebSockexTest do frame = {:text, "Hello"} send(context.server_pid, {:send, frame}) - message = "No handle_frame/2 clause in #{__MODULE__}.BareClient provided for #{inspect frame}" + message = + "No handle_frame/2 clause in #{__MODULE__}.BareClient provided for #{inspect(frame)}" + assert_receive {:EXIT, _, {%RuntimeError{message: ^message}, _}} end @@ -1351,9 +1436,9 @@ defmodule WebSockexTest do import ExUnit.CaptureLog assert capture_log(fn -> - send context.pid, :info - Process.sleep(50) - end) =~ "No handle_info/2 clause in #{__MODULE__}.BareClient provided for :info" + send(context.pid, :info) + Process.sleep(50) + end) =~ "No handle_info/2 clause in #{__MODULE__}.BareClient provided for :info" end end @@ -1378,11 +1463,12 @@ defmodule WebSockexTest do send(context.server_pid, :connection_wait) send(context.server_pid, :close) - _new_server_pid = WebSockex.TestServer.receive_socket_pid + _new_server_pid = WebSockex.TestServer.receive_socket_pid() - {:data, data} = elem(:sys.get_status(pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", :connecting} in data @@ -1395,13 +1481,12 @@ defmodule WebSockexTest do Process.flag(:trap_exit, true) send(context.server_pid, :connection_wait) - {:ok, pid} = TestClient.start_link(context.url, - %{catch_terminate: self()}, - async: true) + {:ok, pid} = TestClient.start_link(context.url, %{catch_terminate: self()}, async: true) - {:data, data} = elem(:sys.get_status(pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", :connecting} in data @@ -1415,9 +1500,10 @@ defmodule WebSockexTest do {:ok, pid} = WebSockex.start_link(context.url, BareClient, [], async: true) - {:data, data} = elem(:sys.get_status(pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", :connecting} in data @@ -1428,9 +1514,10 @@ defmodule WebSockexTest do send(new_server_pid, :send_ping) assert_receive :received_pong - {:data, data} = elem(:sys.get_status(pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", :connected} in data end @@ -1440,9 +1527,10 @@ defmodule WebSockexTest do TestClient.catch_attr(context.pid, :terminate, self()) WebSockex.cast(context.pid, :close) - {:data, data} = elem(:sys.get_status(context.pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(context.pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", {:closing, {:local, :normal}}} in data @@ -1456,9 +1544,10 @@ defmodule WebSockexTest do send(context.server_pid, :stall) - {:data, data} = elem(:sys.get_status(pid), 3) - |> List.flatten - |> List.keyfind(:data, 0) + {:data, data} = + elem(:sys.get_status(pid), 3) + |> List.flatten() + |> List.keyfind(:data, 0) assert {"Connection Status", {:closing, {:local, :normal}}} in data @@ -1469,7 +1558,7 @@ defmodule WebSockexTest do end test ":sys.replace_state only replaces module_state", context do - :sys.replace_state(context.pid, fn(_) -> :lemon end) + :sys.replace_state(context.pid, fn _ -> :lemon end) WebSockex.cast(context.pid, {:get_state, self()}) assert_receive :lemon @@ -1485,9 +1574,10 @@ defmodule WebSockexTest do # Other `format_status` stuff in tested in callback section test ":sys.get_status returns from format_status", context do - {{:data, data}, rest} = elem(:sys.get_status(context.pid), 3) - |> List.last - |> List.keytake(:data, 0) + {{:data, data}, rest} = + elem(:sys.get_status(context.pid), 3) + |> List.last() + |> List.keytake(:data, 0) assert {"Connection Status", :connected} in data @@ -1499,12 +1589,12 @@ defmodule WebSockexTest do describe "child_spec" do test "child_spec/2" do assert %{id: TestClient, start: {TestClient, :start_link, ["url", :state]}} = - TestClient.child_spec("url", :state) + TestClient.child_spec("url", :state) end test "child_spec/1" do assert %{id: TestClient, start: {TestClient, :start_link, [:state]}} = - TestClient.child_spec(:state) + TestClient.child_spec(:state) end end