ws_handler_SUITE.erl 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. %% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(ws_handler_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(cowboy_test, [gun_open/1]).
  20. -import(cowboy_test, [gun_down/1]).
  21. %% ct.
  22. all() ->
  23. [{group, ws}, {group, ws_hibernate}].
  24. %% @todo Test against HTTP/2 too.
  25. groups() ->
  26. AllTests = ct_helper:all(?MODULE),
  27. [{ws, [parallel], AllTests}, {ws_hibernate, [parallel], AllTests}].
  28. init_per_group(Name, Config) ->
  29. cowboy_test:init_http(Name, #{
  30. env => #{dispatch => init_dispatch(Name)}
  31. }, Config).
  32. end_per_group(Name, _) ->
  33. cowboy:stop_listener(Name).
  34. %% Dispatch configuration.
  35. init_dispatch(Name) ->
  36. RunOrHibernate = case Name of
  37. ws -> run;
  38. ws_hibernate -> hibernate
  39. end,
  40. cowboy_router:compile([{'_', [
  41. {"/init", ws_init_commands_h, RunOrHibernate},
  42. {"/handle", ws_handle_commands_h, RunOrHibernate},
  43. {"/info", ws_info_commands_h, RunOrHibernate},
  44. {"/active", ws_active_commands_h, RunOrHibernate},
  45. {"/deflate", ws_deflate_commands_h, RunOrHibernate}
  46. ]}]).
  47. %% Support functions for testing using Gun.
  48. gun_open_ws(Config, Path, Commands) ->
  49. ConnPid = gun_open(Config),
  50. StreamRef = gun:ws_upgrade(ConnPid, Path, [
  51. {<<"x-commands">>, base64:encode(term_to_binary(Commands))}
  52. ]),
  53. receive
  54. {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
  55. {ok, ConnPid, StreamRef};
  56. {gun_response, ConnPid, _, _, Status, Headers} ->
  57. exit({ws_upgrade_failed, Status, Headers});
  58. {gun_error, ConnPid, StreamRef, Reason} ->
  59. exit({ws_upgrade_failed, Reason})
  60. after 1000 ->
  61. error(timeout)
  62. end.
  63. receive_ws(ConnPid, StreamRef) ->
  64. receive
  65. {gun_ws, ConnPid, StreamRef, Frame} ->
  66. {ok, Frame}
  67. after 1000 ->
  68. {error, timeout}
  69. end.
  70. ensure_handle_is_called(ConnPid, "/handle") ->
  71. gun:ws_send(ConnPid, {text, <<"Necessary to trigger websocket_handle/2.">>});
  72. ensure_handle_is_called(_, _) ->
  73. ok.
  74. %% Tests.
  75. websocket_init_nothing(Config) ->
  76. doc("Nothing happens when websocket_init/1 returns no commands."),
  77. do_nothing(Config, "/init").
  78. websocket_handle_nothing(Config) ->
  79. doc("Nothing happens when websocket_handle/2 returns no commands."),
  80. do_nothing(Config, "/handle").
  81. websocket_info_nothing(Config) ->
  82. doc("Nothing happens when websocket_info/2 returns no commands."),
  83. do_nothing(Config, "/info").
  84. do_nothing(Config, Path) ->
  85. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, []),
  86. ensure_handle_is_called(ConnPid, Path),
  87. {error, timeout} = receive_ws(ConnPid, StreamRef),
  88. ok.
  89. websocket_init_invalid(Config) ->
  90. doc("The connection must be closed when websocket_init/1 returns an invalid command."),
  91. do_invalid(Config, "/init").
  92. websocket_handle_invalid(Config) ->
  93. doc("The connection must be closed when websocket_handle/2 returns an invalid command."),
  94. do_invalid(Config, "/init").
  95. websocket_info_invalid(Config) ->
  96. doc("The connection must be closed when websocket_info/2 returns an invalid command."),
  97. do_invalid(Config, "/info").
  98. do_invalid(Config, Path) ->
  99. {ok, ConnPid, _} = gun_open_ws(Config, Path, bad),
  100. ensure_handle_is_called(ConnPid, Path),
  101. gun_down(ConnPid).
  102. websocket_init_one_frame(Config) ->
  103. doc("A single frame is received when websocket_init/1 returns it as a command."),
  104. do_one_frame(Config, "/init").
  105. websocket_handle_one_frame(Config) ->
  106. doc("A single frame is received when websocket_handle/2 returns it as a command."),
  107. do_one_frame(Config, "/handle").
  108. websocket_info_one_frame(Config) ->
  109. doc("A single frame is received when websocket_info/2 returns it as a command."),
  110. do_one_frame(Config, "/info").
  111. do_one_frame(Config, Path) ->
  112. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  113. {text, <<"One frame!">>}
  114. ]),
  115. ensure_handle_is_called(ConnPid, Path),
  116. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  117. ok.
  118. websocket_init_many_frames(Config) ->
  119. doc("Multiple frames are received when websocket_init/1 returns them as commands."),
  120. do_many_frames(Config, "/init").
  121. websocket_handle_many_frames(Config) ->
  122. doc("Multiple frames are received when websocket_handle/2 returns them as commands."),
  123. do_many_frames(Config, "/handle").
  124. websocket_info_many_frames(Config) ->
  125. doc("Multiple frames are received when websocket_info/2 returns them as commands."),
  126. do_many_frames(Config, "/info").
  127. do_many_frames(Config, Path) ->
  128. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  129. {text, <<"One frame!">>},
  130. {binary, <<"Two frames!">>}
  131. ]),
  132. ensure_handle_is_called(ConnPid, Path),
  133. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  134. {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
  135. ok.
  136. websocket_init_close_frame(Config) ->
  137. doc("A single close frame is received when websocket_init/1 returns it as a command."),
  138. do_close_frame(Config, "/init").
  139. websocket_handle_close_frame(Config) ->
  140. doc("A single close frame is received when websocket_handle/2 returns it as a command."),
  141. do_close_frame(Config, "/handle").
  142. websocket_info_close_frame(Config) ->
  143. doc("A single close frame is received when websocket_info/2 returns it as a command."),
  144. do_close_frame(Config, "/info").
  145. do_close_frame(Config, Path) ->
  146. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [close]),
  147. ensure_handle_is_called(ConnPid, Path),
  148. {ok, close} = receive_ws(ConnPid, StreamRef),
  149. gun_down(ConnPid).
  150. websocket_init_many_frames_then_close_frame(Config) ->
  151. doc("Multiple frames are received followed by a close frame "
  152. "when websocket_init/1 returns them as commands."),
  153. do_many_frames_then_close_frame(Config, "/init").
  154. websocket_handle_many_frames_then_close_frame(Config) ->
  155. doc("Multiple frames are received followed by a close frame "
  156. "when websocket_handle/2 returns them as commands."),
  157. do_many_frames_then_close_frame(Config, "/handle").
  158. websocket_info_many_frames_then_close_frame(Config) ->
  159. doc("Multiple frames are received followed by a close frame "
  160. "when websocket_info/2 returns them as commands."),
  161. do_many_frames_then_close_frame(Config, "/info").
  162. do_many_frames_then_close_frame(Config, Path) ->
  163. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  164. {text, <<"One frame!">>},
  165. {binary, <<"Two frames!">>},
  166. close
  167. ]),
  168. ensure_handle_is_called(ConnPid, Path),
  169. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  170. {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
  171. {ok, close} = receive_ws(ConnPid, StreamRef),
  172. gun_down(ConnPid).
  173. websocket_active_false(Config) ->
  174. doc("The {active, false} command stops receiving data from the socket. "
  175. "The {active, true} command reenables it."),
  176. {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/active", []),
  177. gun:ws_send(ConnPid, {text, <<"Not received until the handler enables active again.">>}),
  178. {error, timeout} = receive_ws(ConnPid, StreamRef),
  179. {ok, {text, <<"Not received until the handler enables active again.">>}}
  180. = receive_ws(ConnPid, StreamRef),
  181. ok.
  182. websocket_deflate_false(Config) ->
  183. doc("The {deflate, false} command temporarily disables compression. "
  184. "The {deflate, true} command reenables it."),
  185. %% We disable context takeover so that the compressed data
  186. %% does not change across all frames.
  187. {ok, Socket, Headers} = ws_SUITE:do_handshake("/deflate",
  188. "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover\r\n", Config),
  189. {_, "permessage-deflate; server_no_context_takeover"}
  190. = lists:keyfind("sec-websocket-extensions", 1, Headers),
  191. %% The handler receives a compressed "Hello" frame and
  192. %% sends back a compressed or uncompressed echo intermittently.
  193. Mask = 16#11223344,
  194. CompressedHello = <<242, 72, 205, 201, 201, 7, 0>>,
  195. MaskedHello = ws_SUITE:do_mask(CompressedHello, Mask, <<>>),
  196. %% First echo is compressed.
  197. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>),
  198. {ok, <<1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary>>} = gen_tcp:recv(Socket, 0, 6000),
  199. %% Second echo is not compressed when it is received back.
  200. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>),
  201. {ok, <<1:1, 0:3, 1:4, 0:1, 5:7, "Hello">>} = gen_tcp:recv(Socket, 0, 6000),
  202. %% Third echo is compressed again.
  203. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>),
  204. {ok, <<1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary>>} = gen_tcp:recv(Socket, 0, 6000),
  205. %% Client-initiated close.
  206. ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>),
  207. {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
  208. {error, closed} = gen_tcp:recv(Socket, 0, 6000),
  209. ok.
  210. websocket_deflate_ignore_if_not_negotiated(Config) ->
  211. doc("The {deflate, boolean()} commands are ignored "
  212. "when compression was not negotiated."),
  213. {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/deflate", []),
  214. _ = [begin
  215. gun:ws_send(ConnPid, {text, <<"Hello.">>}),
  216. {ok, {text, <<"Hello.">>}} = receive_ws(ConnPid, StreamRef)
  217. end || _ <- lists:seq(1, 10)],
  218. ok.