From e53416de2a8bde355dcaf26f6c00e79a4c2195ab Mon Sep 17 00:00:00 2001 From: Ino Murko Date: Thu, 8 Nov 2018 22:34:04 +0100 Subject: [PATCH] updated dependencies --- .credo.exs | 159 ++++++++++++++++++++++++++++++++++ .formatter.exs | 4 +- .tool-versions | 2 + examples/chrome_debbuger.exs | 27 +++--- examples/echo_client.exs | 11 ++- lib/websockex.ex | 49 +++++------ lib/websockex/application.ex | 4 + lib/websockex/conn.ex | 16 +++- lib/websockex/errors.ex | 22 ++--- lib/websockex/frame.ex | 97 ++++++++++----------- lib/websockex/utils.ex | 91 ++++++++++++++----- mix.exs | 15 +++- mix.lock | 26 ++++-- test/support/test_server.ex | 123 ++++++++++---------------- test/websockex/conn_test.exs | 18 ++-- test/websockex/frame_test.exs | 97 +++++++++------------ test/websockex_test.exs | 100 ++++++++------------- 17 files changed, 512 insertions(+), 349 deletions(-) create mode 100644 .credo.exs create mode 100644 .tool-versions diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..dc4b745 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,159 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any exec using `mix credo -C `. If no exec name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: ["lib/", "src/", "test/", "web/", "apps/"], + excluded: [~r"/_build/", ~r"/deps/"] + }, + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames}, + {Credo.Check.Consistency.LineEndings}, + {Credo.Check.Consistency.ParameterPatternMatching}, + {Credo.Check.Consistency.SpaceAroundOperators, false}, + {Credo.Check.Consistency.SpaceInParentheses}, + {Credo.Check.Consistency.TabsOrSpaces}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, false}, + # For some checks, you can also set other parameters + # + # If you don't want the `setup` and `test` macro calls in ExUnit tests + # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just + # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. + # + {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, exit_status: 2}, + {Credo.Check.Design.TagFIXME}, + + # + ## Readability Checks + # + {Credo.Check.Readability.FunctionNames}, + {Credo.Check.Readability.LargeNumbers}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120}, + {Credo.Check.Readability.ModuleAttributeNames}, + {Credo.Check.Readability.ModuleDoc, false}, + {Credo.Check.Readability.AliasOrder}, + {Credo.Check.Readability.ModuleNames}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, + {Credo.Check.Readability.ParenthesesInCondition}, + {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.PreferImplicitTry}, + {Credo.Check.Readability.RedundantBlankLines}, + {Credo.Check.Readability.StringSigils}, + {Credo.Check.Readability.TrailingBlankLine}, + {Credo.Check.Readability.TrailingWhiteSpace}, + {Credo.Check.Readability.VariableNames}, + {Credo.Check.Readability.Semicolons}, + {Credo.Check.Readability.SpaceAfterCommas}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.DoubleBooleanNegation}, + {Credo.Check.Refactor.CondStatements}, + {Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 10}, + {Credo.Check.Refactor.FunctionArity, max_arity: 12}, + {Credo.Check.Refactor.LongQuoteBlocks}, + {Credo.Check.Refactor.MatchInCondition}, + {Credo.Check.Refactor.NegatedConditionsInUnless}, + {Credo.Check.Refactor.NegatedConditionsWithElse}, + {Credo.Check.Refactor.Nesting, max_nesting: 3}, + {Credo.Check.Refactor.PipeChainStart, :false}, + {Credo.Check.Refactor.UnlessWithElse}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, + {Credo.Check.Warning.IExPry}, + {Credo.Check.Warning.IoInspect}, + {Credo.Check.Warning.LazyLogging}, + {Credo.Check.Warning.OperationOnSameValues}, + {Credo.Check.Warning.OperationWithConstantResult}, + {Credo.Check.Warning.UnusedEnumOperation}, + {Credo.Check.Warning.UnusedFileOperation}, + {Credo.Check.Warning.UnusedKeywordOperation}, + {Credo.Check.Warning.UnusedListOperation}, + {Credo.Check.Warning.UnusedPathOperation}, + {Credo.Check.Warning.UnusedRegexOperation}, + {Credo.Check.Warning.UnusedStringOperation}, + {Credo.Check.Warning.UnusedTupleOperation}, + {Credo.Check.Warning.RaiseInsideRescue}, + + # + # Controversial and experimental checks (opt-in, just remove `, false`) + # + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + + # + # Deprecated checks (these will be deleted after a grace period) + # + {Credo.Check.Readability.Specs, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index 2bed17c..531b7dc 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,5 @@ [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] + line_length: 108, + rename_deprecated_at: "1.3.0", + inputs: [".formatter.exs", "mix.exs", "{examples, config,lib,test}/**/*.{ex,exs}"] ] diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..9770aa1 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.1 +elixir 1.7.3-otp-21 diff --git a/examples/chrome_debbuger.exs b/examples/chrome_debbuger.exs index d7b92f1..22df340 100644 --- a/examples/chrome_debbuger.exs +++ b/examples/chrome_debbuger.exs @@ -9,7 +9,7 @@ defmodule ChromeDebugger do ChromeDebugger.start("ws://127.0.0.1:55171/devtools/page/18569def-3e03-4d67-a5b9-ca6a0ee0db77", %{port: 55171}) ``` """ - + def start(url, state) do {:ok, pid} = WebSockex.start(url, __MODULE__, state) WebSockex.send_frame(pid, {:text, Poison.encode!(%{id: 1, method: "Network.enable", params: %{}})}) @@ -19,29 +19,34 @@ defmodule ChromeDebugger do end def terminate(reason, state) do - IO.puts("WebSockex for remote debbugging on port #{state.port} terminating with reason: #{inspect reason}") + IO.puts( + "WebSockex for remote debbugging on port #{state.port} terminating with reason: #{inspect(reason)}" + ) + exit(:normal) end def handle_frame({_type, msg}, state) do case Poison.decode(msg) do {:error, error} -> - Logger.debug(inspect msg) - Logger.error(inspect error) + Logger.debug(inspect(msg)) + Logger.error(inspect(error)) + {:ok, message} -> case message["method"] do "Network.webSocketFrameReceived" -> with true <- message["params"]["response"]["opcode"] == 1, - message_pl <- message["params"]["response"]["payloadData"] - do - IO.puts("Message from remote-debbuging port #{state.port}: #{inspect message_pl}") - else - _ -> :ok + message_pl <- message["params"]["response"]["payloadData"] do + IO.puts("Message from remote-debbuging port #{state.port}: #{inspect(message_pl)}") + else + _ -> :ok end - _ -> :ok + + _ -> + :ok end end + {:ok, state} end - end diff --git a/examples/echo_client.exs b/examples/echo_client.exs index fdbd331..ebe9fc6 100644 --- a/examples/echo_client.exs +++ b/examples/echo_client.exs @@ -6,7 +6,7 @@ defmodule EchoClient do WebSockex.start_link("wss://echo.websocket.org/?encoding=text", __MODULE__, :fake_state, opts) end - @spec echo(pid, String.t) :: :ok + @spec echo(pid, String.t()) :: :ok def echo(client, message) do Logger.info("Sending message: #{message}") WebSockex.send_frame(client, {:text, message}) @@ -23,19 +23,22 @@ defmodule EchoClient do Logger.info("Sending message: #{msg}") {:reply, {:text, msg}, :fake_state} end + def handle_frame({:text, "Close the things!" = msg}, :fake_state) do Logger.info("Received Message: #{msg}") {:close, :fake_state} end + def handle_frame({:text, msg}, :fake_state) do Logger.info("Received Message: #{msg}") {:ok, :fake_state} end def handle_disconnect(%{reason: {:local, reason}}, state) do - Logger.info("Local close with reason: #{inspect reason}") + Logger.info("Local close with reason: #{inspect(reason)}") {:ok, state} end + def handle_disconnect(disconnect_map, state) do super(disconnect_map, state) end @@ -47,8 +50,8 @@ EchoClient.echo(pid, "Yo Homies!") EchoClient.echo(pid, "This and That!") EchoClient.echo(pid, "Can you please reply yourself?") -Process.sleep 1000 +Process.sleep(1000) EchoClient.echo(pid, "Close the things!") -Process.sleep 1500 +Process.sleep(1500) diff --git a/lib/websockex.ex b/lib/websockex.ex index 31c901a..940ef59 100644 --- a/lib/websockex.ex +++ b/lib/websockex.ex @@ -48,9 +48,7 @@ defmodule WebSockex do @type client :: pid | atom | {:via, module, term} | {:global, term} @type frame :: - :ping - | :pong - | {:ping | :pong, nil | message :: binary} + {:ping | :ping, nil | message :: binary} | {:text | :binary, message :: binary} @typedoc """ @@ -341,8 +339,7 @@ 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 @@ -375,7 +372,7 @@ defmodule WebSockex do {:ok, pid} | {:error, term} def start_link(conn_info, module, state, opts \\ []) - def start_link(conn = %WebSockex.Conn{}, module, state, opts) do + def start_link(%WebSockex.Conn{} = conn, module, state, opts) do Utils.spawn(:link, conn, module, state, opts) end @@ -423,13 +420,11 @@ defmodule WebSockex do end def send_frame(client, frame) do - try do - {:ok, res} = :gen.call(client, :"$websockex_send", frame) - res - catch - _, reason -> - exit({reason, {__MODULE__, :call, [client, frame]}}) - end + {:ok, res} = :gen.call(client, :"$websockex_send", frame) + res + rescue + reason -> + exit({reason, {__MODULE__, :call, [client, frame]}}) end @doc false @@ -523,7 +518,7 @@ 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) -> @@ -589,7 +584,7 @@ defmodule WebSockex do :sys.handle_system_msg(req, from, parent, __MODULE__, debug, state) {:"$websockex_send", from, _frame} -> - :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :opening}}) + _ = :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :opening}}) open_loop(parent, debug, state) {:EXIT, ^parent, reason} -> @@ -676,7 +671,7 @@ defmodule WebSockex do close_loop(reason, parent, debug, state) {:"$websockex_send", from, _frame} -> - :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :closing}}) + _ = :gen.reply(from, {:error, %WebSockex.NotConnectedError{connection_state: :closing}}) close_loop(reason, parent, debug, state) {close_mod, ^socket} when close_mod in [:tcp_closed, :ssl_closed] -> @@ -686,6 +681,13 @@ defmodule WebSockex do state = Map.delete(state, :timer_ref) on_disconnect(reason, parent, debug, %{state | conn: new_conn}) + {:tcp_closed, ^socket} -> + new_conn = %{conn | socket: nil} + debug = Utils.sys_debug(debug, :closed, state) + 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) @@ -804,7 +806,7 @@ defmodule WebSockex do # 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) + do: _ = WebSockex.Conn.socket_send(state.conn, binary_frame) case res do :ok -> @@ -819,10 +821,7 @@ defmodule WebSockex do 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) @@ -868,7 +867,7 @@ defmodule WebSockex do end defp handle_error_close(reason, parent, debug, state) do - send_close_frame(:error, state.conn) + _ = send_close_frame(:error, state.conn) timer_ref = Process.send_after(self(), :"$websockex_close_timeout", 5000) close_loop(reason, parent, debug, Map.put(state, :timer_ref, timer_ref)) @@ -899,16 +898,16 @@ defmodule WebSockex do case res do :ok -> - :gen.reply(from, :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) + _ = :gen.reply(from, error) handle_close(error, parent, debug, state) {:error, _} = error -> - :gen.reply(from, error) + _ = :gen.reply(from, error) websocket_loop(parent, debug, state) end end diff --git a/lib/websockex/application.ex b/lib/websockex/application.ex index e6111eb..78e86e5 100644 --- a/lib/websockex/application.ex +++ b/lib/websockex/application.ex @@ -1,6 +1,10 @@ defmodule WebSockex.Application do use Application + @moduledoc """ + Start as an OTP application to set URI default sockets and start ssl and + crypto + """ # Start as an OTP application to set URI default sockets and start ssl and # crypto @doc """ diff --git a/lib/websockex/conn.ex b/lib/websockex/conn.ex index 044befc..d822391 100644 --- a/lib/websockex/conn.ex +++ b/lib/websockex/conn.ex @@ -90,8 +90,7 @@ defmodule WebSockex.Conn do 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_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), ssl_options: Keyword.get(opts, :ssl_options, nil) } @@ -297,8 +296,17 @@ defmodule WebSockex.Conn do {:ok, {:http_response, _version, 101, _message}, rest} -> decode_headers(rest) - {:ok, {:http_response, _, code, message}, _} -> - {:error, %WebSockex.RequestError{code: code, message: message}} + {:ok, {:http_response, version, code, message}, rest} -> + {:ok, headers, body} = decode_headers(rest) + + {:error, + %WebSockex.RequestError{ + version: version, + code: code, + message: message, + headers: headers, + body: body + }} {:error, error} -> {:error, error} diff --git a/lib/websockex/errors.ex b/lib/websockex/errors.ex index 725ff24..a1551dd 100644 --- a/lib/websockex/errors.ex +++ b/lib/websockex/errors.ex @@ -14,19 +14,15 @@ 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: :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] + defexception [:version, :code, :message, :headers, :body] 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 @@ -70,9 +66,7 @@ defmodule WebSockex.FrameError do 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 @@ -80,9 +74,7 @@ defmodule WebSockex.FrameError do 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 @@ -103,9 +95,7 @@ defmodule WebSockex.FrameEncodeError do 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 diff --git a/lib/websockex/frame.ex b/lib/websockex/frame.ex index 4686c7c..88b1f37 100644 --- a/lib/websockex/frame.ex +++ b/lib/websockex/frame.ex @@ -33,7 +33,19 @@ defmodule WebSockex.Frame do 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{ + buffer: binary(), + opcode: :close | :ping | :pong | :text, + reason: + :close_with_single_byte_payload + | :control_frame_too_large + | :invalid_close_code + | :invalid_utf8 + | :nonfin_control_frame + }} def parse_frame(data) when bit_size(data) < 16 do :incomplete end @@ -47,26 +59,17 @@ defmodule WebSockex.Frame do # 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 - }} + %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 - }} + %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 @@ -76,16 +79,12 @@ defmodule WebSockex.Frame do 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>> - ) + 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>> - ) + 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 @@ -93,18 +92,12 @@ defmodule WebSockex.Frame do # 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 + <<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 @@ -160,16 +153,12 @@ defmodule WebSockex.Frame do # 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 @@ -215,19 +204,28 @@ 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} + ) :: + {:ok, {:binary, binary()}} + | {:error, + %WebSockex.FragmentParseError{ + reason: :invalid_utf8 | :two_start_frames, + fragment: {:fragment, any(), any()}, + continuation: {:fragment, any(), any()} + }} + | {:error, + %WebSockex.FrameError{ + reason: :invalid_utf8 | :two_start_frames, + opcode: :text, + buffer: binary() + }} 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 @@ -372,17 +370,10 @@ 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 diff --git a/lib/websockex/utils.ex b/lib/websockex/utils.ex index d8bfeb1..0e1a7e0 100644 --- a/lib/websockex/utils.ex +++ b/lib/websockex/utils.ex @@ -39,12 +39,10 @@ defmodule WebSockex.Utils do end def register(name) do - try do - Process.register(self(), name) - catch - :error, _ -> - {:error, {:already_started, Process.whereis(name)}} - end + Process.register(self(), name) + rescue + _ -> + {:error, {:already_started, Process.whereis(name)}} end def send({:global, name}, msg), do: WebSockex.Utils.send({:via, :global, name}, msg) @@ -77,19 +75,31 @@ defmodule WebSockex.Utils do 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 @@ -107,19 +117,31 @@ defmodule WebSockex.Utils do 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 @@ -130,31 +152,52 @@ defmodule WebSockex.Utils do 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 @@ -164,7 +207,11 @@ defmodule WebSockex.Utils 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 diff --git a/mix.exs b/mix.exs index ab69f66..1694ae8 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,11 @@ defmodule WebSockex.Mixfile do elixirc_paths: elixirc_paths(Mix.env()), package: package(), deps: deps(), - docs: docs() + docs: docs(), + dialyzer: [ + flags: [:underspecs, :unknown, :unmatched_returns], + plt_add_apps: [:mix, :iex, :ex_unit, :ranch, :plug, :websockex, :cowboy] + ] ] end @@ -27,9 +31,12 @@ defmodule WebSockex.Mixfile do 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.19", only: :dev, runtime: false}, + {:cowboy, "~> 2.5"}, + {:plug, "~> 1.7"}, + {:plug_cowboy, "~> 2.0"}, + {:credo, "~> 1.0.0-rc1", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.0.0-rc.4", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 24e1c12..4488504 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,19 @@ -%{"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, - "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, - "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, - "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, - "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, - "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}} +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.5.0", "4ef3ae066ee10fe01ea3272edc8f024347a0d3eb95f6fbb9aed556dacbfc1337", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.6.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm"}, + "credo": {:hex, :credo, "1.0.0-rc1", "31655e89925d3a75d27fd6a06aaffa853017cf0d9411645e3a56974db950669b", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, + "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.0.0", "ab0c92728f2ba43c544cce85f0f220d8d30fc0c90eaa1e6203683ab039655062", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "ranch": {:hex, :ranch, "1.6.2", "6db93c78f411ee033dbb18ba8234c5574883acb9a75af0fb90a9b82ea46afa00", [:rebar3], [], "hexpm"}, +} diff --git a/test/support/test_server.ex b/test/support/test_server.ex index 956f5fc..7c368d5 100644 --- a/test/support/test_server.ex +++ b/test/support/test_server.ex @@ -1,11 +1,11 @@ defmodule WebSockex.TestServer do Module.register_attribute(__MODULE__, :dialyzer, persist: true) + @moduledoc false use Plug.Router - + alias Plug.Adapters.Cowboy @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) @@ -22,7 +22,7 @@ defmodule WebSockex.TestServer do opts = [dispatch: dispatch({pid, agent_pid}), port: port, ref: ref] - case Plug.Adapters.Cowboy.http(__MODULE__, [], opts) do + case Cowboy.http(__MODULE__, [], opts) do {:ok, _} -> {:ok, {ref, url}} @@ -45,7 +45,7 @@ defmodule WebSockex.TestServer do ref: ref ] - case Plug.Adapters.Cowboy.https(__MODULE__, [], opts) do + case Cowboy.https(__MODULE__, [], opts) do {:ok, _} -> {:ok, {ref, url}} @@ -56,9 +56,7 @@ defmodule WebSockex.TestServer do end end - def shutdown(ref) do - Plug.Adapters.Cowboy.shutdown(ref) - end + def shutdown(ref), do: Cowboy.shutdown(ref) def receive_socket_pid do receive do @@ -89,12 +87,12 @@ defmodule WebSockex.TestServer do end defmodule WebSockex.TestSocket do - @behaviour :cowboy_websocket_handler + @behaviour :cowboy_websocket - def init(_, req, [{test_pid, agent_pid}]) do + def init(req, [{test_pid, agent_pid}] = state) do case Agent.get(agent_pid, fn x -> x end) do :ok -> - {:upgrade, :protocol, :cowboy_websocket} + {:cowboy_websocket, req, state} int when is_integer(int) -> :cowboy_req.reply(int, req) @@ -105,118 +103,93 @@ defmodule WebSockex.TestSocket do receive do :connection_continue -> - {:upgrade, :protocol, :cowboy_websocket} + {:cowboy_websocket, req, state} end - - :immediate_reply -> - immediate_reply(req) end end - def terminate(_, _, _), do: :ok - - def websocket_init(_, req, [{test_pid, agent_pid}]) do + def websocket_init([{test_pid, agent_pid}]) do send(test_pid, self()) - {:ok, req, %{pid: test_pid, agent_pid: agent_pid}} + {:ok, %{pid: test_pid, agent_pid: agent_pid}} end - def websocket_terminate({:remote, :closed}, _, state) do + def terminate(:remote, _, state) do send(state.pid, :normal_remote_closed) end - def websocket_terminate({:remote, close_code, reason}, _, state) do + def terminate({:remote, :closed}, _, state) do + send(state.pid, :normal_remote_closed) + end + + def terminate({:remote, close_code, reason}, _, state) do send(state.pid, {close_code, reason}) end - def websocket_terminate(_, _, _) do + def terminate(_, _, _) do :ok end - def websocket_handle({:binary, msg}, req, state) do + def websocket_handle({:binary, msg}, state) do send(state.pid, :erlang.binary_to_term(msg)) - {:ok, req, state} + {:ok, state} end - def websocket_handle({:ping, _}, req, state), do: {:ok, req, state} + def websocket_handle({:ping, _}, state), do: {:ok, state} - def websocket_handle({:pong, ""}, req, state) do + def websocket_handle(:pong, state) do send(state.pid, :received_pong) - {:ok, req, state} + {:ok, state} end - def websocket_handle({:pong, payload}, req, %{ping_payload: ping_payload} = state) + def websocket_handle({:pong, payload}, %{ping_payload: ping_payload} = state) when payload == ping_payload do send(state.pid, :received_payload_pong) - {:ok, req, state} + {:ok, state} end - def websocket_info(:stall, _, _) 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_ping, state), do: {:reply, :ping, state} - def websocket_info(:send_payload_ping, req, state) do + def websocket_info(:send_payload_ping, state) do payload = "Llama and Lambs" - {:reply, {:ping, payload}, req, Map.put(state, :ping_payload, payload)} + {:reply, {:ping, payload}, Map.put(state, :ping_payload, payload)} end - def websocket_info(:close, req, state), do: {:reply, :close, req, state} + def websocket_info(:close, state), do: {:reply, :close, state} + + def websocket_info({:close, reason}, state) do + {:reply, {:close, reason}, state} + end - def websocket_info({:close, code, reason}, req, state) do - {:reply, {:close, code, reason}, req, state} + def websocket_info({:close, code, reason}, state) do + {:reply, {:close, code, reason}, state} end - def websocket_info({:send, frame}, req, state) do - {:reply, frame, req, state} + def websocket_info({:send, frame}, state) do + {:reply, frame, state} end - def websocket_info({:set_code, code}, req, state) do + def websocket_info({:set_code, code}, state) do Agent.update(state.agent_pid, fn _ -> code end) - {:ok, req, state} + {:ok, state} end - def websocket_info(:connection_wait, req, state) do + def websocket_info(:connection_wait, state) do Agent.update(state.agent_pid, fn _ -> :connection_wait end) - {:ok, req, state} + {:ok, state} end - def websocket_info(:immediate_reply, req, state) do + def websocket_info(:immediate_reply, state) do Agent.update(state.agent_pid, fn _ -> :immediate_reply end) - {:ok, req, state} + {:ok, state} end - def websocket_info(:shutdown, req, state) do - {:shutdown, req, state} + def websocket_info(:shutdown, state) do + {:shutdown, state} end - def websocket_info(_, req, state), do: {:ok, req, state} - - @dialyzer {:nowarn_function, immediate_reply: 1} - defp immediate_reply(req) do - socket = elem(req, 1) - 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") - - frame = <<1::1, 0::3, 1::4, 0::1, 15::7, "Immediate Reply">> - transport.send(socket, handshake) - Process.sleep(0) - transport.send(socket, frame) - - Process.sleep(:infinity) - end + def websocket_info(_, state), do: {:ok, state} end diff --git a/test/websockex/conn_test.exs b/test/websockex/conn_test.exs index 4134e84..e124d26 100644 --- a/test/websockex/conn_test.exs +++ b/test/websockex/conn_test.exs @@ -57,8 +57,7 @@ defmodule WebSockex.ConnTest do socket_recv_timeout: 456 } - assert WebSockex.Conn.new(regular_url, regular_opts) == - WebSockex.Conn.new(regular_uri, regular_opts) + assert WebSockex.Conn.new(regular_url, regular_opts) == WebSockex.Conn.new(regular_uri, regular_opts) conn_opts = [extra_headers: [{"Pineapple", "Cake"}]] @@ -98,8 +97,7 @@ defmodule WebSockex.ConnTest do end test "parse_url" do - assert WebSockex.Conn.parse_url("lemon_pie") == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + assert WebSockex.Conn.parse_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)} @@ -132,7 +130,14 @@ 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", + body: "", + headers: ["Content-Length": "0", Connection: "close"], + version: {1, 1} + }} end describe "secure connection" do @@ -150,8 +155,7 @@ defmodule WebSockex.ConnTest do test "open_socket with supplied cacerts", context do conn = - WebSockex.Conn.new( - context.uri, + WebSockex.Conn.new(context.uri, insecure: false, cacerts: WebSockex.TestServer.cacerts() ) diff --git a/test/websockex/frame_test.exs b/test/websockex/frame_test.exs index 3c219df..22d49d9 100644 --- a/test/websockex/frame_test.exs +++ b/test/websockex/frame_test.exs @@ -218,8 +218,7 @@ defmodule WebSockex.FrameTest 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 @@ -248,24 +247,21 @@ defmodule WebSockex.FrameTest 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 @@ -302,8 +298,7 @@ defmodule WebSockex.FrameTest do <> = 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 @@ -315,9 +310,7 @@ defmodule WebSockex.FrameTest do test "Finishes a binary fragment" do <> = @binary - - assert Frame.parse_fragment({:fragment, :binary, part}, {:finish, rest}) == - {:ok, {:binary, @binary}} + assert Frame.parse_fragment({:fragment, :binary, part}, {:finish, rest}) == {:ok, {:binary, @binary}} end end @@ -331,8 +324,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -346,8 +339,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -361,8 +354,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -413,8 +406,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -425,8 +417,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -436,8 +428,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -446,8 +438,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -458,8 +449,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -469,8 +460,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -479,8 +470,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -491,8 +481,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -502,8 +492,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -512,8 +502,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -524,8 +513,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -535,8 +524,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -545,8 +534,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -557,8 +545,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -568,8 +556,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -578,8 +566,7 @@ defmodule WebSockex.FrameTest 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>>} = + 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 @@ -590,8 +577,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 @@ -601,8 +588,8 @@ defmodule WebSockex.FrameTest do 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}) + <<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 diff --git a/test/websockex_test.exs b/test/websockex_test.exs index 75842b9..39c77ee 100644 --- a/test/websockex_test.exs +++ b/test/websockex_test.exs @@ -198,10 +198,7 @@ defmodule WebSockexTest do {: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)} @@ -218,10 +215,7 @@ defmodule WebSockexTest do {:reconnect, state} end - def handle_disconnect( - %{reason: {:remote, :closed}}, - %{catch_disconnect: pid, reconnect: true} = state - ) do + def handle_disconnect(%{reason: {:remote, :closed}}, %{catch_disconnect: pid, reconnect: true} = state) do send(pid, {:caught_disconnect, :reconnecting}) {:reconnect, state} end @@ -238,7 +232,10 @@ defmodule WebSockexTest do 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 @@ -385,8 +382,7 @@ defmodule WebSockexTest do end test "returns an error with a bad url" do - assert TestClient.start_link("lemon_pie", :ok) == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + assert TestClient.start_link("lemon_pie", :ok) == {:error, %WebSockex.URLError{url: "lemon_pie"}} end end @@ -428,8 +424,7 @@ defmodule WebSockexTest do end test "returns an error with a bad url" do - assert TestClient.start_link("lemon_pie", :ok) == - {:error, %WebSockex.URLError{url: "lemon_pie"}} + assert TestClient.start_link("lemon_pie", :ok) == {:error, %WebSockex.URLError{url: "lemon_pie"}} end end @@ -468,29 +463,6 @@ defmodule WebSockexTest do assert_receive :caught_pong end - test "handles a tcp message send right after connecting", context do - send(context.server_pid, :immediate_reply) - - assert {:ok, _pid} = TestClient.start_link(context.url, %{catch_text: self()}) - - assert_receive {:caught_text, "Immediate Reply"} - end - - 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) - - {:ok, _pid} = TestClient.start_link(url, %{}) - server_pid = WebSockex.TestServer.receive_socket_pid() - - send(server_pid, :immediate_reply) - - assert {:ok, _pid} = TestClient.start_link(url, %{catch_text: self()}) - - assert_receive {:caught_text, "Immediate Reply"} - end - test "handle changes state", context do rand_number = :rand.uniform(1000) @@ -606,16 +578,11 @@ defmodule WebSockexTest do # Really glad that I got those sys behaviors now :sys.suspend(pid) - test_pid = self() - task = Task.async(fn -> - send(test_pid, :task_started) WebSockex.send_frame(pid, {:text, "hello"}) end) - assert_receive :task_started - :gen_tcp.shutdown(conn.socket, :write) :sys.resume(pid) @@ -1231,13 +1198,11 @@ 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 @@ -1341,29 +1306,20 @@ defmodule WebSockexTest do handle_initial_conn_failure: true ) - 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 - ) + TestClient.start_link(context.url <> "bad", state_map, handle_initial_conn_failure: true) server_pid = WebSockex.TestServer.receive_socket_pid() @@ -1434,8 +1390,18 @@ defmodule WebSockexTest do end test "Won't exit on a request error", context do + {:error, request_error} = TestClient.start_link(context.url <> "blah", %{}) + [Server: "Cowboy", Date: date, "Content-Length": "0"] = request_error.headers + assert TestClient.start_link(context.url <> "blah", %{}) == - {:error, %WebSockex.RequestError{code: 404, message: "Not Found"}} + {:error, + %WebSockex.RequestError{ + code: 404, + message: "Not Found", + body: "", + headers: [Server: "Cowboy", Date: date, "Content-Length": "0"], + version: {1, 1} + }} end describe "default implementation errors" do @@ -1451,9 +1417,7 @@ 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 @@ -1514,7 +1478,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) @@ -1541,8 +1510,10 @@ defmodule WebSockexTest do assert {"Connection Status", :connecting} in data new_server_pid = WebSockex.TestServer.receive_socket_pid() + send(new_server_pid, :connection_continue) - ^new_server_pid = WebSockex.TestServer.receive_socket_pid() + + new_server_pid = WebSockex.TestServer.receive_socket_pid() send(new_server_pid, :send_ping) assert_receive :received_pong @@ -1626,8 +1597,7 @@ defmodule WebSockexTest do end test "child_spec/1" do - assert %{id: TestClient, start: {TestClient, :start_link, [:state]}} = - TestClient.child_spec(:state) + assert %{id: TestClient, start: {TestClient, :start_link, [:state]}} = TestClient.child_spec(:state) end end