/test/ws_SUITE.erl
Erlang | 544 lines | 449 code | 49 blank | 46 comment | 2 complexity | ecac07f7aef6d84e4370fadc53a3e018 MD5 | raw file
1%% Copyright (c) 2011-2017, 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 15-module(ws_SUITE). 16-compile(export_all). 17 18-import(ct_helper, [config/2]). 19-import(ct_helper, [doc/1]). 20 21%% ct. 22 23all() -> 24 [{group, autobahn}, {group, ws}]. 25 26groups() -> 27 BaseTests = ct_helper:all(?MODULE) -- [autobahn_fuzzingclient], 28 [{autobahn, [], [autobahn_fuzzingclient]}, {ws, [parallel], BaseTests}]. 29 30init_per_group(Name = autobahn, Config) -> 31 %% Some systems have it named pip2. 32 Out = os:cmd("pip show autobahntestsuite ; pip2 show autobahntestsuite"), 33 case string:str(Out, "autobahntestsuite") of 34 0 -> 35 ct:print("Skipping the autobahn group because the " 36 "Autobahn Test Suite is not installed.~nTo install it, " 37 "please follow the instructions on this page:~n~n " 38 "http://autobahn.ws/testsuite/installation.html"), 39 {skip, "Autobahn Test Suite not installed."}; 40 _ -> 41 {ok, _} = cowboy:start_clear(Name, [{port, 33080}], #{ 42 env => #{dispatch => init_dispatch()} 43 }), 44 Config 45 end; 46init_per_group(Name = ws, Config) -> 47 cowboy_test:init_http(Name, #{ 48 env => #{dispatch => init_dispatch()} 49 }, Config). 50 51end_per_group(Listener, _Config) -> 52 cowboy:stop_listener(Listener). 53 54%% Dispatch configuration. 55 56init_dispatch() -> 57 cowboy_router:compile([ 58 {"localhost", [ 59 {"/ws_echo", ws_echo, []}, 60 {"/ws_echo_timer", ws_echo_timer, []}, 61 {"/ws_init", ws_init_h, []}, 62 {"/ws_init_shutdown", ws_init_shutdown, []}, 63 {"/ws_send_many", ws_send_many, [ 64 {sequence, [ 65 {text, <<"one">>}, 66 {text, <<"two">>}, 67 {text, <<"seven!">>}]} 68 ]}, 69 {"/ws_send_close", ws_send_many, [ 70 {sequence, [ 71 {text, <<"send">>}, 72 close, 73 {text, <<"won't be received">>}]} 74 ]}, 75 {"/ws_send_close_payload", ws_send_many, [ 76 {sequence, [ 77 {text, <<"send">>}, 78 {close, 1001, <<"some text!">>}, 79 {text, <<"won't be received">>}]} 80 ]}, 81 {"/ws_subprotocol", ws_subprotocol, []}, 82 {"/terminate", ws_terminate_h, []}, 83 {"/ws_timeout_hibernate", ws_timeout_hibernate, []}, 84 {"/ws_timeout_cancel", ws_timeout_cancel, []} 85 ]} 86 ]). 87 88%% Tests. 89 90autobahn_fuzzingclient(Config) -> 91 doc("Autobahn test suite for the Websocket protocol."), 92 Self = self(), 93 spawn_link(fun() -> start_port(Config, Self) end), 94 receive autobahn_exit -> ok end, 95 ct:log("<h2><a href=\"log_private/reports/servers/index.html\">Full report</a></h2>~n"), 96 Report = config(priv_dir, Config) ++ "reports/servers/index.html", 97 ct:print("Autobahn Test Suite report: file://~s~n", [Report]), 98 {ok, HTML} = file:read_file(Report), 99 case length(binary:matches(HTML, <<"case_failed">>)) > 2 of 100 true -> error(failed); 101 false -> ok 102 end. 103 104start_port(Config, Pid) -> 105 Port = open_port({spawn, "wstest -m fuzzingclient -s " ++ config(data_dir, Config) ++ "client.json"}, 106 [{line, 10000}, {cd, config(priv_dir, Config)}, binary, eof]), 107 receive_infinity(Port, Pid). 108 109receive_infinity(Port, Pid) -> 110 receive 111 {Port, {data, {eol, Line}}} -> 112 io:format(user, "~s~n", [Line]), 113 receive_infinity(Port, Pid); 114 {Port, eof} -> 115 Pid ! autobahn_exit 116 end. 117 118ws0(Config) -> 119 doc("Websocket version 0 (hixie-76 draft) is no longer supported."), 120 {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), 121 ok = gen_tcp:send(Socket, 122 "GET /ws_echo_timer HTTP/1.1\r\n" 123 "Host: localhost\r\n" 124 "Connection: Upgrade\r\n" 125 "Upgrade: WebSocket\r\n" 126 "Origin: http://localhost\r\n" 127 "Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i\r\n" 128 "Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n" 129 "\r\n"), 130 {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), 131 {ok, {http_response, {1, 1}, 400, _}, _} = erlang:decode_packet(http, Handshake, []), 132 ok. 133 134ws7(Config) -> 135 doc("Websocket version 7 (draft) is supported."), 136 {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), 137 ok = gen_tcp:send(Socket, [ 138 "GET /ws_echo_timer HTTP/1.1\r\n" 139 "Host: localhost\r\n" 140 "Connection: Upgrade\r\n" 141 "Upgrade: websocket\r\n" 142 "Sec-WebSocket-Origin: http://localhost\r\n" 143 "Sec-WebSocket-Version: 7\r\n" 144 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" 145 "\r\n"]), 146 {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), 147 {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), 148 [Headers, <<>>] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), 149 {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), 150 {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), 151 {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), 152 do_ws_version(Socket). 153 154ws8(Config) -> 155 doc("Websocket version 8 (draft) is supported."), 156 {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), 157 ok = gen_tcp:send(Socket, [ 158 "GET /ws_echo_timer HTTP/1.1\r\n" 159 "Host: localhost\r\n" 160 "Connection: Upgrade\r\n" 161 "Upgrade: websocket\r\n" 162 "Sec-WebSocket-Origin: http://localhost\r\n" 163 "Sec-WebSocket-Version: 8\r\n" 164 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" 165 "\r\n"]), 166 {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), 167 {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), 168 [Headers, <<>>] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), 169 {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), 170 {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), 171 {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), 172 do_ws_version(Socket). 173 174ws13(Config) -> 175 doc("Websocket version 13 (RFC) is supported."), 176 {ok, Socket, _} = do_handshake("/ws_echo_timer", Config), 177 do_ws_version(Socket). 178 179do_ws_version(Socket) -> 180 %% Masked text hello echoed back clear by the server. 181 Mask = 16#37fa213d, 182 MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), 183 ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), 184 {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 185 %% Empty binary frame echoed back. 186 ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 0:7, 0:32 >>), 187 {ok, << 1:1, 0:3, 2:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 188 %% Masked binary hello echoed back clear by the server. 189 ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), 190 {ok, << 1:1, 0:3, 2:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 191 %% Frames sent on timer by the handler. 192 {ok, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>} = gen_tcp:recv(Socket, 0, 6000), 193 {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), 194 {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), 195 {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), 196 %% Client-initiated ping/pong. 197 ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 1:1, 0:7, 0:32 >>), 198 {ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 199 %% Client-initiated close. 200 ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), 201 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 202 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 203 ok. 204 205ws_init_return_ok(Config) -> 206 doc("Handler does nothing."), 207 {ok, Socket, _} = do_handshake("/ws_init?ok", Config), 208 %% The handler does nothing; nothing should happen here. 209 {error, timeout} = gen_tcp:recv(Socket, 0, 1000), 210 ok. 211 212ws_init_return_ok_hibernate(Config) -> 213 doc("Handler does nothing; hibernates."), 214 {ok, Socket, _} = do_handshake("/ws_init?ok_hibernate", Config), 215 %% The handler does nothing; nothing should happen here. 216 {error, timeout} = gen_tcp:recv(Socket, 0, 1000), 217 ok. 218 219ws_init_return_reply(Config) -> 220 doc("Handler sends a text frame just after the handshake."), 221 {ok, Socket, _} = do_handshake("/ws_init?reply", Config), 222 {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 223 ok. 224 225ws_init_return_reply_hibernate(Config) -> 226 doc("Handler sends a text frame just after the handshake and then hibernates."), 227 {ok, Socket, _} = do_handshake("/ws_init?reply_hibernate", Config), 228 {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 229 ok. 230 231ws_init_return_reply_close(Config) -> 232 doc("Handler closes immediately after the handshake."), 233 {ok, Socket, _} = do_handshake("/ws_init?reply_close", Config), 234 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 235 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 236 ok. 237 238ws_init_return_reply_close_hibernate(Config) -> 239 doc("Handler closes immediately after the handshake, then attempts to hibernate."), 240 {ok, Socket, _} = do_handshake("/ws_init?reply_close_hibernate", Config), 241 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 242 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 243 ok. 244 245ws_init_return_reply_many(Config) -> 246 doc("Handler sends many frames just after the handshake."), 247 {ok, Socket, _} = do_handshake("/ws_init?reply_many", Config), 248 %% We catch all frames at once and check them directly. 249 {ok, << 250 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 251 1:1, 0:3, 2:4, 0:1, 5:7, "World" >>} = gen_tcp:recv(Socket, 14, 6000), 252 ok. 253 254ws_init_return_reply_many_hibernate(Config) -> 255 doc("Handler sends many frames just after the handshake and then hibernates."), 256 {ok, Socket, _} = do_handshake("/ws_init?reply_many_hibernate", Config), 257 %% We catch all frames at once and check them directly. 258 {ok, << 259 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 260 1:1, 0:3, 2:4, 0:1, 5:7, "World" >>} = gen_tcp:recv(Socket, 14, 6000), 261 ok. 262 263ws_init_return_reply_many_close(Config) -> 264 doc("Handler sends many frames including a close frame just after the handshake."), 265 {ok, Socket, _} = do_handshake("/ws_init?reply_many_close", Config), 266 %% We catch all frames at once and check them directly. 267 {ok, << 268 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 269 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 9, 6000), 270 ok. 271 272ws_init_return_reply_many_close_hibernate(Config) -> 273 doc("Handler sends many frames including a close frame just after the handshake and then hibernates."), 274 {ok, Socket, _} = do_handshake("/ws_init?reply_many_close_hibernate", Config), 275 %% We catch all frames at once and check them directly. 276 {ok, << 277 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 278 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 9, 6000), 279 ok. 280 281ws_init_return_stop(Config) -> 282 doc("Handler closes immediately after the handshake."), 283 {ok, Socket, _} = do_handshake("/ws_init?stop", Config), 284 {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), 285 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 286 ok. 287 288ws_init_shutdown_before_handshake(Config) -> 289 doc("Handler stops before Websocket handshake."), 290 {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), 291 ok = gen_tcp:send(Socket, [ 292 "GET /ws_init_shutdown HTTP/1.1\r\n" 293 "Host: localhost\r\n" 294 "Connection: Upgrade\r\n" 295 "Origin: http://localhost\r\n" 296 "Sec-WebSocket-Version: 13\r\n" 297 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" 298 "Upgrade: websocket\r\n" 299 "\r\n"]), 300 {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), 301 {ok, {http_response, {1, 1}, 403, _}, _Rest} = erlang:decode_packet(http, Handshake, []), 302 ok. 303 304ws_send_close(Config) -> 305 doc("Server-initiated close frame ends the connection."), 306 {ok, Socket, _} = do_handshake("/ws_send_close", Config), 307 %% We catch all frames at once and check them directly. 308 {ok, << 309 1:1, 0:3, 1:4, 0:1, 4:7, "send", 310 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 8, 6000), 311 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 312 ok. 313 314ws_send_close_payload(Config) -> 315 doc("Server-initiated close frame with payload ends the connection."), 316 {ok, Socket, _} = do_handshake("/ws_send_close_payload", Config), 317 %% We catch all frames at once and check them directly. 318 {ok, << 319 1:1, 0:3, 1:4, 0:1, 4:7, "send", 320 1:1, 0:3, 8:4, 0:1, 12:7, 1001:16, "some text!" >>} = gen_tcp:recv(Socket, 20, 6000), 321 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 322 ok. 323 324ws_send_many(Config) -> 325 doc("Server sends many frames in a single reply."), 326 {ok, Socket, _} = do_handshake("/ws_send_many", Config), 327 %% We catch all frames at once and check them directly. 328 {ok, << 329 1:1, 0:3, 1:4, 0:1, 3:7, "one", 330 1:1, 0:3, 1:4, 0:1, 3:7, "two", 331 1:1, 0:3, 1:4, 0:1, 6:7, "seven!" >>} = gen_tcp:recv(Socket, 18, 6000), 332 ok. 333 334ws_single_bytes(Config) -> 335 doc("Client sends a text frame one byte at a time."), 336 {ok, Socket, _} = do_handshake("/ws_echo", Config), 337 %% We sleep between sends to make sure only one byte is sent. 338 ok = gen_tcp:send(Socket, << 16#81 >>), timer:sleep(100), 339 ok = gen_tcp:send(Socket, << 16#85 >>), timer:sleep(100), 340 ok = gen_tcp:send(Socket, << 16#37 >>), timer:sleep(100), 341 ok = gen_tcp:send(Socket, << 16#fa >>), timer:sleep(100), 342 ok = gen_tcp:send(Socket, << 16#21 >>), timer:sleep(100), 343 ok = gen_tcp:send(Socket, << 16#3d >>), timer:sleep(100), 344 ok = gen_tcp:send(Socket, << 16#7f >>), timer:sleep(100), 345 ok = gen_tcp:send(Socket, << 16#9f >>), timer:sleep(100), 346 ok = gen_tcp:send(Socket, << 16#4d >>), timer:sleep(100), 347 ok = gen_tcp:send(Socket, << 16#51 >>), timer:sleep(100), 348 ok = gen_tcp:send(Socket, << 16#58 >>), 349 {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 350 ok. 351 352ws_subprotocol(Config) -> 353 doc("Websocket sub-protocol negotiation."), 354 {ok, _, Headers} = do_handshake("/ws_subprotocol", 355 "Sec-WebSocket-Protocol: foo, bar\r\n", Config), 356 {_, "foo"} = lists:keyfind("sec-websocket-protocol", 1, Headers), 357 ok. 358 359ws_terminate(Config) -> 360 doc("The Req object is kept in a more compact form by default."), 361 {ok, Socket, _} = do_handshake("/terminate", 362 "x-test-pid: " ++ pid_to_list(self()) ++ "\r\n", Config), 363 %% Send a close frame. 364 ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), 365 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 366 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 367 %% Confirm terminate/3 was called with a compacted Req. 368 receive {terminate, _, Req} -> 369 true = maps:is_key(path, Req), 370 false = maps:is_key(headers, Req), 371 ok 372 after 1000 -> 373 error(timeout) 374 end. 375 376ws_terminate_fun(Config) -> 377 doc("A function can be given to filter the Req object."), 378 {ok, Socket, _} = do_handshake("/terminate?req_filter", 379 "x-test-pid: " ++ pid_to_list(self()) ++ "\r\n", Config), 380 %% Send a close frame. 381 ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), 382 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 383 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 384 %% Confirm terminate/3 was called with a compacted Req. 385 receive {terminate, _, Req} -> 386 filtered = Req, 387 ok 388 after 1000 -> 389 error(timeout) 390 end. 391 392ws_text_fragments(Config) -> 393 doc("Client sends fragmented text frames."), 394 {ok, Socket, _} = do_handshake("/ws_echo", Config), 395 %% Send two "Hello" over two fragments and two sends. 396 Mask = 16#37fa213d, 397 MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), 398 ok = gen_tcp:send(Socket, << 0:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), 399 ok = gen_tcp:send(Socket, << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), 400 {ok, << 1:1, 0:3, 1:4, 0:1, 10:7, "HelloHello" >>} = gen_tcp:recv(Socket, 0, 6000), 401 %% Send three "Hello" over three fragments and one send. 402 ok = gen_tcp:send(Socket, [ 403 << 0:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>, 404 << 0:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>, 405 << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>]), 406 {ok, << 1:1, 0:3, 1:4, 0:1, 15:7, "HelloHelloHello" >>} = gen_tcp:recv(Socket, 0, 6000), 407 ok. 408 409ws_timeout_hibernate(Config) -> 410 doc("Server-initiated close on timeout with hibernating process."), 411 {ok, Socket, _} = do_handshake("/ws_timeout_hibernate", Config), 412 {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), 413 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 414 ok. 415 416ws_timeout_no_cancel(Config) -> 417 doc("Server-initiated timeout is not influenced by reception of Erlang messages."), 418 {ok, Socket, _} = do_handshake("/ws_timeout_cancel", Config), 419 {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), 420 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 421 ok. 422 423ws_timeout_reset(Config) -> 424 doc("Server-initiated timeout is reset when client sends more data."), 425 {ok, Socket, _} = do_handshake("/ws_timeout_cancel", Config), 426 %% Send and receive back a frame a few times. 427 Mask = 16#37fa213d, 428 MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), 429 [begin 430 ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), 431 {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), 432 timer:sleep(500) 433 end || _ <- [1, 2, 3, 4]], 434 %% Timeout will occur after we stop sending data. 435 {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), 436 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 437 ok. 438 439ws_webkit_deflate(Config) -> 440 doc("x-webkit-deflate-frame compression."), 441 {ok, Socket, Headers} = do_handshake("/ws_echo", 442 "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), 443 {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), 444 %% Send and receive a compressed "Hello" frame. 445 Mask = 16#11223344, 446 CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, 447 MaskedHello = do_mask(CompressedHello, Mask, <<>>), 448 ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary >>), 449 {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), 450 %% Client-initiated close. 451 ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), 452 {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), 453 {error, closed} = gen_tcp:recv(Socket, 0, 6000), 454 ok. 455 456ws_webkit_deflate_fragments(Config) -> 457 doc("Client sends an x-webkit-deflate-frame compressed and fragmented text frame."), 458 {ok, Socket, Headers} = do_handshake("/ws_echo", 459 "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), 460 {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), 461 %% Send a compressed "Hello" over two fragments and two sends. 462 Mask = 16#11223344, 463 CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, 464 MaskedHello1 = do_mask(binary:part(CompressedHello, 0, 4), Mask, <<>>), 465 MaskedHello2 = do_mask(binary:part(CompressedHello, 4, 3), Mask, <<>>), 466 ok = gen_tcp:send(Socket, << 0:1, 1:1, 0:2, 1:4, 1:1, 4:7, Mask:32, MaskedHello1/binary >>), 467 ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 0:4, 1:1, 3:7, Mask:32, MaskedHello2/binary >>), 468 {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), 469 ok. 470 471ws_webkit_deflate_single_bytes(Config) -> 472 doc("Client sends an x-webkit-deflate-frame compressed text frame one byte at a time."), 473 {ok, Socket, Headers} = do_handshake("/ws_echo", 474 "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), 475 {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), 476 %% We sleep between sends to make sure only one byte is sent. 477 Mask = 16#11223344, 478 CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, 479 MaskedHello = do_mask(CompressedHello, Mask, <<>>), 480 ok = gen_tcp:send(Socket, << 16#c1 >>), timer:sleep(100), 481 ok = gen_tcp:send(Socket, << 16#87 >>), timer:sleep(100), 482 ok = gen_tcp:send(Socket, << 16#11 >>), timer:sleep(100), 483 ok = gen_tcp:send(Socket, << 16#22 >>), timer:sleep(100), 484 ok = gen_tcp:send(Socket, << 16#33 >>), timer:sleep(100), 485 ok = gen_tcp:send(Socket, << 16#44 >>), timer:sleep(100), 486 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 0)]), timer:sleep(100), 487 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 1)]), timer:sleep(100), 488 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 2)]), timer:sleep(100), 489 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 3)]), timer:sleep(100), 490 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 4)]), timer:sleep(100), 491 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 5)]), timer:sleep(100), 492 ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 6)]), 493 {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), 494 ok. 495 496%% Internal. 497 498do_handshake(Path, Config) -> 499 do_handshake(Path, "", Config). 500 501do_handshake(Path, ExtraHeaders, Config) -> 502 {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), 503 [binary, {active, false}]), 504 ok = gen_tcp:send(Socket, [ 505 "GET ", Path, " HTTP/1.1\r\n" 506 "Host: localhost\r\n" 507 "Connection: Upgrade\r\n" 508 "Origin: http://localhost\r\n" 509 "Sec-WebSocket-Version: 13\r\n" 510 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" 511 "Upgrade: websocket\r\n", 512 ExtraHeaders, 513 "\r\n"]), 514 {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), 515 {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), 516 [Headers, <<>>] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), 517 {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), 518 {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), 519 {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), 520 {ok, Socket, Headers}. 521 522do_decode_headers({ok, http_eoh, Rest}, Acc) -> 523 [Acc, Rest]; 524do_decode_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) -> 525 F = fun(S) when is_atom(S) -> S; (S) -> string:to_lower(S) end, 526 do_decode_headers(erlang:decode_packet(httph, Rest, []), [{F(Key), Value}|Acc]). 527 528do_mask(<<>>, _, Acc) -> 529 Acc; 530do_mask(<< O:32, Rest/bits >>, MaskKey, Acc) -> 531 T = O bxor MaskKey, 532 do_mask(Rest, MaskKey, << Acc/binary, T:32 >>); 533do_mask(<< O:24 >>, MaskKey, Acc) -> 534 << MaskKey2:24, _:8 >> = << MaskKey:32 >>, 535 T = O bxor MaskKey2, 536 << Acc/binary, T:24 >>; 537do_mask(<< O:16 >>, MaskKey, Acc) -> 538 << MaskKey2:16, _:16 >> = << MaskKey:32 >>, 539 T = O bxor MaskKey2, 540 << Acc/binary, T:16 >>; 541do_mask(<< O:8 >>, MaskKey, Acc) -> 542 << MaskKey2:8, _:24 >> = << MaskKey:32 >>, 543 T = O bxor MaskKey2, 544 << Acc/binary, T:8 >>.