From 9e200eee4b1fa0a2c92d37592c51da0e56ad84cf Mon Sep 17 00:00:00 2001 From: Justin Baker Date: Thu, 8 Feb 2018 19:25:26 -0600 Subject: [PATCH] Fix a crash when replying to a closed socket --- lib/websockex.ex | 2 +- test/websockex_test.exs | 89 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/lib/websockex.ex b/lib/websockex.ex index 6d6a486..f1c2d8a 100644 --- a/lib/websockex.ex +++ b/lib/websockex.ex @@ -701,7 +701,7 @@ defmodule WebSockex do {: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: :ok = WebSockex.Conn.socket_send(state.conn, binary_frame) + do: WebSockex.Conn.socket_send(state.conn, binary_frame) case res do :ok -> debug = Utils.sys_debug(debug, {:reply, function, frame}, state) diff --git a/test/websockex_test.exs b/test/websockex_test.exs index cb4d550..d29f8ed 100644 --- a/test/websockex_test.exs +++ b/test/websockex_test.exs @@ -132,6 +132,7 @@ defmodule WebSockexTest do 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, "Please Reply"}, state), do: {:reply, {:pong, "No"}, state} def handle_ping(frame, state), do: super(frame, state) # Implicitly test default implementation defined with using through super @@ -146,6 +147,7 @@ defmodule WebSockexTest do 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, "Please Reply"}, state), do: {:reply, {:text, "No"}, state} def handle_frame({:binary, msg}, %{catch_binary: pid} = state) do send(pid, {:caught_binary, msg}) @@ -155,6 +157,7 @@ defmodule WebSockexTest 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" @@ -642,6 +645,22 @@ defmodule WebSockexTest do assert_receive {1011, ""} assert_receive {:EXIT, _, %WebSockex.InvalidFrameError{}} end + + test "handles dead connections when replying", context do + Process.flag(:trap_exit, true) + %{socket: socket} = TestClient.get_conn(context.pid) + TestClient.catch_attr(context.pid, :disconnect, self()) + + :sys.suspend(context.pid) + + WebSockex.cast(context.pid, {:send, {:text, "It's Closed"}}) + :gen_tcp.shutdown(socket, :write) + + :sys.resume(context.pid) + + assert_receive {:EXIT, _, %WebSockex.ConnError{original: :closed}} + assert_received :caught_disconnect + end end describe "handle_connect callback" do @@ -677,6 +696,24 @@ defmodule WebSockexTest do assert_receive {:caught_text, ^text} end + + test "handles dead connections when replying", context do + Process.flag(:trap_exit, true) + conn = TestClient.get_conn(context.pid) + TestClient.catch_attr(context.pid, :disconnect, self()) + + :sys.suspend(context.pid) + + frame = <<1::1, 0::3, 1::4, 0::1, 12::7, "Please Reply"::utf8>> + send(context.pid, {conn.transport, conn.socket, frame}) + + :gen_tcp.shutdown(conn.socket, :write) + + :sys.resume(context.pid) + + assert_receive {:EXIT, _, %WebSockex.ConnError{original: :closed}} + assert_received :caught_disconnect + end end describe "handle_info callback" do @@ -706,6 +743,22 @@ defmodule WebSockexTest do assert_receive {:EXIT, _, {:local, 4012, "Test Close"}} assert_receive {4012, "Test Close"} end + + test "handles dead connections when replying", context do + Process.flag(:trap_exit, true) + %{socket: socket} = TestClient.get_conn(context.pid) + TestClient.catch_attr(context.pid, :disconnect, self()) + + :sys.suspend(context.pid) + + send(context.pid, {:send, {:text, "It's Closed"}}) + :gen_tcp.shutdown(socket, :write) + + :sys.resume(context.pid) + + assert_receive {:EXIT, _, %WebSockex.ConnError{original: :closed}} + assert_received :caught_disconnect + end end describe "terminate callback" do @@ -973,6 +1026,24 @@ defmodule WebSockexTest do assert_receive :received_payload_pong end + + test "handles dead connections when replying", context do + Process.flag(:trap_exit, true) + conn = TestClient.get_conn(context.pid) + TestClient.catch_attr(context.pid, :disconnect, self()) + + :sys.suspend(context.pid) + + frame = <<1::1, 0::3, 9::4, 0::1, 12::7, "Please Reply"::utf8>> + send(context.pid, {conn.transport, conn.socket, frame}) + + :gen_tcp.shutdown(conn.socket, :write) + + :sys.resume(context.pid) + + assert_receive {:EXIT, _, %WebSockex.ConnError{original: :closed}} + assert_received :caught_disconnect + end end describe "handle_pong callback" do @@ -989,6 +1060,24 @@ defmodule WebSockexTest do assert_receive {:caught_payload_pong, "bananas"} end + + test "handles dead connections when replying", context do + Process.flag(:trap_exit, true) + conn = TestClient.get_conn(context.pid) + TestClient.catch_attr(context.pid, :disconnect, self()) + + :sys.suspend(context.pid) + + frame = <<1::1, 0::3, 10::4, 0::1, 12::7, "Please Reply"::utf8>> + send(context.pid, {conn.transport, conn.socket, frame}) + + :gen_tcp.shutdown(conn.socket, :write) + + :sys.resume(context.pid) + + assert_receive {:EXIT, _, %WebSockex.ConnError{original: :closed}} + assert_received :caught_disconnect + end end describe "disconnects and handle_disconnect callback" do