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