1: %%============================================================================== 2: %% Copyright 2014 Erlang Solutions Ltd. 3: %% 4: %% Licensed under the Apache License, Version 2.0 (the "License"); 5: %% you may not use this file except in compliance with the License. 6: %% You may obtain a copy of the License at 7: %% 8: %% http://www.apache.org/licenses/LICENSE-2.0 9: %% 10: %% Unless required by applicable law or agreed to in writing, software 11: %% distributed under the License is distributed on an "AS IS" BASIS, 12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13: %% See the License for the specific language governing permissions and 14: %% limitations under the License. 15: %%============================================================================== 16: 17: -module(cowboy_SUITE). 18: -compile([export_all, nowarn_export_all]). 19: 20: -include_lib("common_test/include/ct.hrl"). 21: 22: -define(SERVER, "http://localhost:8080"). 23: 24: -import(ejabberd_helper, [start_ejabberd/1, 25: stop_ejabberd/0, 26: use_config_file/2, 27: start_ejabberd_with_config/2]). 28: 29: %%-------------------------------------------------------------------- 30: %% Suite configuration 31: %%-------------------------------------------------------------------- 32: 33: all() -> 34: [{group, routing}, 35: start_cowboy_returns_error_eaddrinuse]. 36: 37: groups() -> 38: [{routing, [sequence], [http_requests, 39: ws_request_bad_protocol, 40: ws_requests_xmpp, 41: ws_requests_other, 42: mixed_requests]}]. 43: 44: suite() -> 45: []. 46: 47: %%-------------------------------------------------------------------- 48: %% Init & teardown 49: %%-------------------------------------------------------------------- 50: 51: -define(APPS, [crypto, ssl, fusco, ranch, cowlib, cowboy]). 52: 53: init_per_suite(Config) -> 54: [application:start(App) || App <- ?APPS], 55: {ok, Pid} = create_handlers(), 56: [{meck_pid, Pid}|Config]. 57: 58: end_per_suite(Config) -> 59: mnesia:stop(), 60: mnesia:delete_schema([node()]), 61: remove_handlers(Config), 62: Config. 63: 64: init_per_group(routing, Config) -> 65: start_cowboy(), 66: Config; 67: init_per_group(_GroupName, Config) -> 68: Config. 69: 70: end_per_group(routing, Config) -> 71: stop_cowboy(), 72: Config; 73: end_per_group(_GroupName, Config) -> 74: Config. 75: 76: init_per_testcase(_CaseName, Config) -> 77: Config. 78: 79: end_per_testcase(_CaseName, Config) -> 80: reset_history(), 81: Config. 82: 83: %%-------------------------------------------------------------------- 84: %% Tests 85: %%-------------------------------------------------------------------- 86: http_requests(_Config) -> 87: %% Given 88: Host = ?SERVER, 89: Path = <<"/">>, 90: Method = "GET", 91: Headers = [], 92: Body = [], 93: NumOfReqs = 50, 94: 95: %% When 96: Codes = [begin 97: Response = execute_request(Host, Path, Method, Headers, Body), 98: to_status_code(Response) 99: end || _ <- lists:seq(1, NumOfReqs)], 100: 101: %% Then 102: ExpectedCodes = lists:duplicate(NumOfReqs, 200), %% NumOfReqs times code 200 103: case Codes of 104: ExpectedCodes -> 105: ok; 106: _ -> 107: ct:fail(#{reason => bad_codes, 108: codes => Codes, 109: expected_codes => ExpectedCodes}) 110: end, 111: assert_cowboy_handler_calls(dummy_http_handler, init, NumOfReqs), 112: assert_cowboy_handler_calls(dummy_http_handler, terminate, NumOfReqs). 113: 114: ws_request_bad_protocol(_Config) -> 115: %% Given 116: Host = ?SERVER, 117: Path = <<"/">>, 118: Method = "GET", 119: Headers = ws_headers(<<"unknown-protocol">>), 120: Body = [], 121: 122: %% When 123: Response = execute_request(Host, Path, Method, Headers, Body), 124: 125: %% Then 126: assert_status_code(Response, 404). 127: 128: ws_requests_xmpp(_Config) -> 129: %% Given 130: Host = "localhost", 131: Port = 8080, 132: Protocol = <<"xmpp">>, 133: BinaryPing = ws_tx_frame(<<"ping">>, 2), 134: BinaryPong = ws_rx_frame(<<"pong">>, 2), 135: 136: %% When 137: {ok, Socket} = ws_handshake(Host, Port, Protocol), 138: Responses = [begin 139: ok = ws_send(Socket, BinaryPing), 140: ws_recv(Socket) 141: end || _ <- lists:seq(1, 50)], 142: ok = gen_tcp:close(Socket), 143: 144: %% Then 145: %% dummy_ws1_handler:init/2 is not called since mod_cowboy takes over 146: Responses = lists:duplicate(50, BinaryPong), 147: assert_cowboy_handler_calls(dummy_ws1_handler, websocket_init, 1), 148: assert_cowboy_handler_calls(dummy_ws1_handler, websocket_handle, 50), 149: ok = meck:wait(dummy_ws1_handler, websocket_terminate, '_', 1000). 150: 151: ws_requests_other(_Config) -> 152: %% Given 153: Host = "localhost", 154: Port = 8080, 155: Protocol = <<"other">>, 156: TextPing = ws_tx_frame(<<"ping">>, 1), 157: TextPong = ws_rx_frame(<<"pong">>, 1), 158: 159: %% When 160: {ok, Socket} = ws_handshake(Host, Port, Protocol), 161: Responses = [begin 162: ok = ws_send(Socket, TextPing), 163: ws_recv(Socket) 164: end || _ <- lists:seq(1, 50)], 165: ok = gen_tcp:close(Socket), 166: 167: %% Then 168: 169: Responses = lists:duplicate(50, TextPong), 170: assert_cowboy_handler_calls(dummy_ws2_handler, websocket_init, 1), 171: assert_cowboy_handler_calls(dummy_ws2_handler, websocket_handle, 50), 172: ok = meck:wait(dummy_ws2_handler, websocket_terminate, '_', 1000). 173: 174: mixed_requests(_Config) -> 175: %% Given 176: Protocol1 = <<"xmpp">>, 177: Protocol2 = <<"other">>, 178: Protocol3 = <<"non-existent">>, 179: 180: TextPing = ws_tx_frame(<<"ping">>, 1), 181: TextPong = ws_rx_frame(<<"pong">>, 1), 182: 183: Host = "localhost", 184: Port = 8080, 185: 186: HTTPHost = ?SERVER, 187: Path = <<"/">>, 188: Method = "GET", 189: Headers3 = ws_headers(Protocol3), 190: Headers4 = [], 191: Body = [], 192: 193: %% When 194: {ok, Socket1} = ws_handshake(Host, Port, Protocol1), 195: {ok, Socket2} = ws_handshake(Host, Port, Protocol2), 196: 197: Responses = [begin 198: ok = ws_send(Socket1, TextPing), 199: Resp1 = ws_recv(Socket1), 200: 201: Resp2 = execute_request(HTTPHost, Path, Method, Headers4, Body), 202: Status2 = is_status_code(Resp2, 200), 203: 204: ok = ws_send(Socket2, TextPing), 205: Resp3 = ws_recv(Socket2), 206: 207: Resp4 = execute_request(HTTPHost, Path, Method, Headers3, Body), 208: Status4 = is_status_code(Resp4, 404), 209: 210: {Resp1, Status2, Resp3, Status4} 211: end || _ <- lists:seq(1, 50)], 212: 213: %% Then 214: Responses = lists:duplicate(50, {TextPong, true, TextPong, true}). 215: 216: start_cowboy_returns_error_eaddrinuse(_C) -> 217: Opts = [{transport_options, #{socket_opts => [{port, 8088}, 218: {ip, {127, 0, 0, 1}}]}}, 219: {modules, []}, 220: {retries, {2, 10}}], 221: {ok, _Pid} = ejabberd_cowboy:start_cowboy(a_ref, Opts), 222: Result = ejabberd_cowboy:start_cowboy(a_ref_2, Opts), 223: {error, eaddrinuse} = Result. 224: 225: %%-------------------------------------------------------------------- 226: %% Helpers 227: %%-------------------------------------------------------------------- 228: copy(Src, Dst) -> 229: {ok, _} = file:copy(Src, Dst). 230: 231: data(Config, Path) -> 232: Dir = proplists:get_value(data_dir, Config), 233: filename:join([Dir, Path]). 234: 235: start_cowboy() -> 236: Dispatch = cowboy_router:compile([ 237: {'_', 238: [{"/[...]", mod_cowboy, 239: [{http, dummy_http_handler}, 240: {ws, xmpp, dummy_ws1_handler}, 241: {ws, other, dummy_ws2_handler} 242: ]}] 243: }]), 244: {ok, _Pid} = cowboy:start_clear(http_listener, 245: #{num_acceptors => 20, 246: socket_opts => [{port, 8080}]}, 247: #{env => #{dispatch => Dispatch}}). 248: 249: stop_cowboy() -> 250: ok = cowboy:stop_listener(http_listener). 251: 252: execute_request(Host, Path, Method, Headers, Body) -> 253: {ok, Pid} = fusco:start_link(Host, []), 254: fusco:request(Pid, Path, Method, Headers, Body, 5000). 255: % We do not disconnect with: 256: % fusco:disconnect(Pid) 257: % due to https://github.com/ninenines/cowboy/issues/1397 258: 259: assert_status_code(Response, Code) -> 260: case is_status_code(Response, Code) of 261: true -> 262: ok; 263: false -> 264: ct:fail(#{reason => assert_status_code, 265: response => Response, 266: expected_code => Code}) 267: end. 268: 269: is_status_code(Response, Code) -> 270: case to_status_code(Response) of 271: Code -> true; 272: _ -> false 273: end. 274: 275: to_status_code({ok, {{CodeBin, _}, _, _, _, _}}) -> 276: binary_to_integer(CodeBin). 277: 278: ws_send(Socket, Frame) -> 279: ok = gen_tcp:send(Socket, Frame). 280: 281: ws_recv(Socket) -> 282: {ok, Packet} = gen_tcp:recv(Socket, 0, 5000), 283: Packet. 284: 285: ws_handshake(Host, Port, Protocol) -> 286: {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, raw}, 287: {active, false}]), 288: ok = gen_tcp:send(Socket, [ 289: "GET / HTTP/1.1\r\n" 290: "Host: localhost\r\n" 291: "Connection: upgrade\r\n" 292: "Origin: http://localhost\r\n" 293: "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" 294: "Sec-WebSocket-Protocol: ", Protocol, "\r\n" 295: "Sec-WebSocket-Version: 13\r\n" 296: "Upgrade: websocket\r\n" 297: "\r\n"]), 298: {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), 299: Packet = erlang:decode_packet(http, Handshake, []), 300: {ok, {http_response, {1,1}, 101, "Switching Protocols"}, _Rest} = Packet, 301: {ok, Socket}. 302: 303: ws_headers(Protocol) -> 304: [{<<"upgrade">>, <<"websocket">>}, 305: {<<"connection">>, <<"upgrade">>}, 306: {<<"sec-websocket-key">>, <<"x3JJHMbDL1EzLkh9GBhXDw==">>}, 307: {<<"sec-websocket-protocol">>, Protocol}, 308: {<<"sec-websocket-version">>, <<"13">>}]. 309: 310: ws_tx_frame(Payload, Opcode) -> 311: Mask = 16#ffffffff, 312: Length = byte_size(Payload), 313: MaskedPayload = << <<(Byte bxor 16#ff):8>> || <<Byte:8>> <= Payload >>, 314: <<1:1, 0:3, Opcode:4, 1:1, Length:7, Mask:32, MaskedPayload/binary>>. 315: 316: ws_rx_frame(Payload, Opcode) -> 317: Length = byte_size(Payload), 318: <<1:1, 0:3, Opcode:4, 0:1, Length:7, Payload/binary>>. 319: 320: %%-------------------------------------------------------------------- 321: %% http/ws handlers mock 322: %%-------------------------------------------------------------------- 323: create_handlers() -> 324: Owner = self(), 325: F = fun() -> 326: [create_handler(Handler) || Handler <- handlers()], 327: Owner ! ok, 328: timer:sleep(infinity) 329: end, 330: Pid = spawn(F), 331: receive 332: ok -> 333: {ok, Pid} 334: after 5000 -> 335: {error, timeout} 336: end. 337: 338: handlers() -> 339: WSFuns = [{init, fun ws_init/2}, 340: {websocket_init, fun ws_websocket_init/1}, 341: {websocket_handle, fun ws_websocket_handle/2}, 342: {websocket_info, fun ws_websocket_info/2}, 343: {websocket_terminate, fun ws_websocket_terminate/3}], 344: [{dummy_http_handler, [{init, fun handler_init/2}, 345: {terminate, fun handler_terminate/3}]}, 346: {dummy_ws1_handler, WSFuns}, 347: {dummy_ws2_handler, WSFuns}]. 348: 349: create_handler({Name, Functions}) -> 350: ok = meck:new(Name, [non_strict]), 351: [ok = meck:expect(Name, Function, Fun) || {Function, Fun} <- Functions]. 352: 353: remove_handlers(Config) -> 354: [ok = meck:unload(Handler) || {Handler, _} <- handlers()], 355: exit(?config(meck_pid, Config), kill). 356: 357: reset_history() -> 358: [ok = meck:reset(Handler) || {Handler, _} <- handlers()]. 359: 360: %% cowboy_http_handler 361: handler_init(Req, _Opts) -> 362: Req1 = cowboy_req:reply(200, Req), 363: {ok, Req1, no_state}. 364: 365: handler_terminate(_Reason, _Req, _State) -> 366: ok. 367: 368: %% cowboy_websocket_handler 369: ws_init(Req, _Opts) -> 370: {cowboy_websocket, Req, no_ws_state}. 371: 372: ws_websocket_init(no_ws_state) -> 373: {ok, no_ws_state}. 374: 375: ws_websocket_handle({text,<<"ping">>}, no_ws_state) -> 376: {reply, {text, <<"pong">>}, no_ws_state}; 377: ws_websocket_handle({binary, <<"ping">>}, no_ws_state) -> 378: {reply, {binary, <<"pong">>}, no_ws_state}; 379: ws_websocket_handle(_Other, no_ws_state) -> 380: {ok, no_ws_state}. 381: 382: ws_websocket_info(_Info, no_ws_state) -> 383: {ok, no_ws_state}. 384: 385: ws_websocket_terminate(_Reason, _Req, no_ws_state) -> 386: ok. 387: 388: assert_cowboy_handler_calls(M, F, Num) -> 389: Fun = fun() -> meck:num_calls(M, F, '_') end, 390: async_helper:wait_until(Fun, Num). 391: