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: