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).