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: -include_lib("eunit/include/eunit.hrl"). 22: 23: -define(SERVER, "http://localhost:8080"). 24: 25: -import(ejabberd_helper, [use_config_file/2, 26: start_ejabberd_with_config/2]). 27: -import(config_parser_helper, [default_config/1]). 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 = #{port => 8088, 218: ip_tuple => {127, 0, 0, 1}, 219: ip_address => "127.0.0.1", 220: handlers => [], 221: transport => default_config([listen, http, transport]), 222: protocol => default_config([listen, http, protocol])}, 223: ?assertMatch({ok, _}, ejabberd_cowboy:start_cowboy(a_ref, Opts, 2, 10)), 224: Result = ejabberd_cowboy:start_cowboy(a_ref_2, Opts, 2, 10), 225: ?assertEqual({error, eaddrinuse}, Result). 226: 227: %%-------------------------------------------------------------------- 228: %% Helpers 229: %%-------------------------------------------------------------------- 230: copy(Src, Dst) -> 231: {ok, _} = file:copy(Src, Dst). 232: 233: data(Config, Path) -> 234: Dir = proplists:get_value(data_dir, Config), 235: filename:join([Dir, Path]). 236: 237: start_cowboy() -> 238: Dispatch = cowboy_router:compile([ 239: {'_', 240: [{"/[...]", mod_cowboy, 241: [{http, dummy_http_handler}, 242: {ws, xmpp, dummy_ws1_handler}, 243: {ws, other, dummy_ws2_handler} 244: ]}] 245: }]), 246: {ok, _Pid} = cowboy:start_clear(http_listener, 247: #{num_acceptors => 20, 248: socket_opts => [{port, 8080}]}, 249: #{env => #{dispatch => Dispatch}}). 250: 251: stop_cowboy() -> 252: ok = cowboy:stop_listener(http_listener). 253: 254: execute_request(Host, Path, Method, Headers, Body) -> 255: {ok, Pid} = fusco:start_link(Host, []), 256: fusco:request(Pid, Path, Method, Headers, Body, 5000). 257: % We do not disconnect with: 258: % fusco:disconnect(Pid) 259: % due to https://github.com/ninenines/cowboy/issues/1397 260: 261: assert_status_code(Response, Code) -> 262: case is_status_code(Response, Code) of 263: true -> 264: ok; 265: false -> 266: ct:fail(#{reason => assert_status_code, 267: response => Response, 268: expected_code => Code}) 269: end. 270: 271: is_status_code(Response, Code) -> 272: case to_status_code(Response) of 273: Code -> true; 274: _ -> false 275: end. 276: 277: to_status_code({ok, {{CodeBin, _}, _, _, _, _}}) -> 278: binary_to_integer(CodeBin). 279: 280: ws_send(Socket, Frame) -> 281: ok = gen_tcp:send(Socket, Frame). 282: 283: ws_recv(Socket) -> 284: {ok, Packet} = gen_tcp:recv(Socket, 0, 5000), 285: Packet. 286: 287: ws_handshake(Host, Port, Protocol) -> 288: {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, raw}, 289: {active, false}]), 290: ok = gen_tcp:send(Socket, [ 291: "GET / HTTP/1.1\r\n" 292: "Host: localhost\r\n" 293: "Connection: upgrade\r\n" 294: "Origin: http://localhost\r\n" 295: "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" 296: "Sec-WebSocket-Protocol: ", Protocol, "\r\n" 297: "Sec-WebSocket-Version: 13\r\n" 298: "Upgrade: websocket\r\n" 299: "\r\n"]), 300: {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), 301: Packet = erlang:decode_packet(http, Handshake, []), 302: {ok, {http_response, {1,1}, 101, "Switching Protocols"}, _Rest} = Packet, 303: {ok, Socket}. 304: 305: ws_headers(Protocol) -> 306: [{<<"upgrade">>, <<"websocket">>}, 307: {<<"connection">>, <<"upgrade">>}, 308: {<<"sec-websocket-key">>, <<"x3JJHMbDL1EzLkh9GBhXDw==">>}, 309: {<<"sec-websocket-protocol">>, Protocol}, 310: {<<"sec-websocket-version">>, <<"13">>}]. 311: 312: ws_tx_frame(Payload, Opcode) -> 313: Mask = 16#ffffffff, 314: Length = byte_size(Payload), 315: MaskedPayload = << <<(Byte bxor 16#ff):8>> || <<Byte:8>> <= Payload >>, 316: <<1:1, 0:3, Opcode:4, 1:1, Length:7, Mask:32, MaskedPayload/binary>>. 317: 318: ws_rx_frame(Payload, Opcode) -> 319: Length = byte_size(Payload), 320: <<1:1, 0:3, Opcode:4, 0:1, Length:7, Payload/binary>>. 321: 322: %%-------------------------------------------------------------------- 323: %% http/ws handlers mock 324: %%-------------------------------------------------------------------- 325: create_handlers() -> 326: Owner = self(), 327: F = fun() -> 328: [create_handler(Handler) || Handler <- handlers()], 329: Owner ! ok, 330: timer:sleep(infinity) 331: end, 332: Pid = spawn(F), 333: receive 334: ok -> 335: {ok, Pid} 336: after 5000 -> 337: {error, timeout} 338: end. 339: 340: handlers() -> 341: WSFuns = [{init, fun ws_init/2}, 342: {websocket_init, fun ws_websocket_init/1}, 343: {websocket_handle, fun ws_websocket_handle/2}, 344: {websocket_info, fun ws_websocket_info/2}, 345: {websocket_terminate, fun ws_websocket_terminate/3}], 346: [{dummy_http_handler, [{init, fun handler_init/2}, 347: {terminate, fun handler_terminate/3}]}, 348: {dummy_ws1_handler, WSFuns}, 349: {dummy_ws2_handler, WSFuns}]. 350: 351: create_handler({Name, Functions}) -> 352: ok = meck:new(Name, [non_strict]), 353: [ok = meck:expect(Name, Function, Fun) || {Function, Fun} <- Functions]. 354: 355: remove_handlers(Config) -> 356: [ok = meck:unload(Handler) || {Handler, _} <- handlers()], 357: exit(?config(meck_pid, Config), kill). 358: 359: reset_history() -> 360: [ok = meck:reset(Handler) || {Handler, _} <- handlers()]. 361: 362: %% cowboy_http_handler 363: handler_init(Req, _Opts) -> 364: Req1 = cowboy_req:reply(200, Req), 365: {ok, Req1, no_state}. 366: 367: handler_terminate(_Reason, _Req, _State) -> 368: ok. 369: 370: %% cowboy_websocket_handler 371: ws_init(Req, _Opts) -> 372: {cowboy_websocket, Req, no_ws_state}. 373: 374: ws_websocket_init(no_ws_state) -> 375: {ok, no_ws_state}. 376: 377: ws_websocket_handle({text,<<"ping">>}, no_ws_state) -> 378: {reply, {text, <<"pong">>}, no_ws_state}; 379: ws_websocket_handle({binary, <<"ping">>}, no_ws_state) -> 380: {reply, {binary, <<"pong">>}, no_ws_state}; 381: ws_websocket_handle(_Other, no_ws_state) -> 382: {ok, no_ws_state}. 383: 384: ws_websocket_info(_Info, no_ws_state) -> 385: {ok, no_ws_state}. 386: 387: ws_websocket_terminate(_Reason, _Req, no_ws_state) -> 388: ok. 389: 390: assert_cowboy_handler_calls(M, F, Num) -> 391: Fun = fun() -> meck:num_calls(M, F, '_') end, 392: async_helper:wait_until(Fun, Num).