1: -module(mod_websockets_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: -include_lib("eunit/include/eunit.hrl").
    4: 
    5: -define(eq(E, I), ?assertEqual(E, I)).
    6: -define(PORT, 5280).
    7: -define(IP, {127, 0, 0, 1}).
    8: -define(FAST_PING_RATE, 500).
    9: -define(IDLE_TIMEOUT, 1200 * 2 + 300).
   10: 
   11: -import(config_parser_helper, [config/2, default_config/1]).
   12: 
   13: all() ->
   14:     [ping_test | subprotocol_header_tests() ++ timeout_tests()].
   15: 
   16: subprotocol_header_tests() ->
   17:     [agree_to_xmpp_subprotocol,
   18:      agree_to_xmpp_subprotocol_case_insensitive,
   19:      agree_to_xmpp_subprotocol_from_many,
   20:      do_not_agree_to_missing_subprotocol,
   21:      do_not_agree_to_other_subprotocol].
   22: 
   23: timeout_tests() ->
   24:     [connection_is_closed_after_idle_timeout,
   25:      client_ping_frame_resets_idle_timeout].
   26: 
   27: init_per_suite(C) ->
   28:     setup(),
   29:     C.
   30: 
   31: end_per_suite(_) ->
   32:     teardown(),
   33:     ok.
   34: 
   35: init_per_testcase(_, C) ->
   36:     C.
   37: 
   38: end_per_testcase(_, C) ->
   39:     C.
   40: 
   41: setup() ->
   42:     meck:unload(),
   43:     application:ensure_all_started(cowboy),
   44:     application:ensure_all_started(jid),
   45:     meck:new(supervisor, [unstick, passthrough, no_link]),
   46:     meck:new(gen_mod,[unstick, passthrough, no_link]),
   47:     %% Set ping rate
   48:     meck:expect(gen_mod,get_opt, fun(ping_rate, _, none) -> ?FAST_PING_RATE;
   49:                                     (A, B, C) -> meck:passthrough([A, B, C]) end),
   50:     meck:expect(supervisor, start_child,
   51:                 fun(mongoose_listener_sup, _) -> {ok, self()};
   52:                    (A, B) -> meck:passthrough([A, B])
   53:                 end),
   54:     mongoose_config:set_opts(#{default_server_name => <<"localhost">>}),
   55:     %% Start websocket cowboy listening
   56:     Handlers = [config([listen, http, handlers, mod_bosh],
   57:                        #{host => '_', path => "/http-bind"}),
   58:                 config([listen, http, handlers, mod_websockets],
   59:                        #{host => '_', path => "/ws-xmpp",
   60:                          timeout => ?IDLE_TIMEOUT, ping_rate => ?FAST_PING_RATE})],
   61:     ejabberd_cowboy:start_listener(#{port => ?PORT,
   62:                                      ip_tuple => ?IP,
   63:                                      ip_address => "127.0.0.1",
   64:                                      ip_version => 4,
   65:                                      proto => tcp,
   66:                                      handlers => Handlers,
   67:                                      transport => default_config([listen, http, transport]),
   68:                                      protocol => default_config([listen, http, protocol])}).
   69: 
   70: teardown() ->
   71:     meck:unload(),
   72:     cowboy:stop_listener(ejabberd_cowboy:ref({?PORT, ?IP, tcp})),
   73:     mongoose_config:erase_opts(),
   74:     application:stop(cowboy),
   75:     %% Do not stop jid, Erlang 21 does not like to reload nifs
   76:     ok.
   77: 
   78: ping_test(_Config) ->
   79:     timer:sleep(500),
   80:     #{socket := Socket1} = ws_handshake(),
   81:     %% When
   82:     Resp = wait_for_ping(Socket1, 0, 5000),
   83:     %% then
   84:     ?eq(Resp, ok).
   85: 
   86: connection_is_closed_after_idle_timeout(_Config) ->
   87:     #{socket := Socket} = ws_handshake(),
   88:     inet:setopts(Socket, [{active, true}]),
   89:     Closed = wait_for_close(Socket),
   90:     ?eq(Closed, ok).
   91: 
   92: client_ping_frame_resets_idle_timeout(_Config) ->
   93:     #{socket := Socket} = ws_handshake(#{extra_headers => [<<"sec-websocket-protocol: xmpp\r\n">>]}),
   94:     Now = os:system_time(millisecond),
   95:     inet:setopts(Socket, [{active, true}]),
   96:     WaitBeforePingFrame = (?IDLE_TIMEOUT) div 2,
   97:     timer:sleep(WaitBeforePingFrame),
   98:     %%Masked ping frame
   99:     Ping = << 1:1, 0:3, 9:4, 1:1, 0:39 >>,
  100:     ok = gen_tcp:send(Socket, Ping),
  101:     Closed = wait_for_close(Socket),
  102:     ?eq(Closed, ok),
  103:     End = os:system_time(millisecond),
  104:     %%Below we check if the time difference between now and the moment
  105:     %%the WebSocket was established is bigger then the the ?IDLE_TIMEOUT plus initial wait time
  106:     %%This shows that the connection was not killed after the first ?IDLE_TIMEOUT
  107:     ?assert(End - Now > ?IDLE_TIMEOUT + WaitBeforePingFrame).
  108: 
  109: wait_for_close(Socket) ->
  110:     receive
  111:         {tcp_closed, Socket} ->
  112:             ok
  113:     after ?IDLE_TIMEOUT + 500 ->
  114:               timeout
  115:     end.
  116: 
  117: 
  118: ws_handshake() ->
  119:     ws_handshake(#{}).
  120: 
  121: %% Client side
  122: %% Gun is too high level for subprotocol_header_tests checks
  123: ws_handshake(Opts) ->
  124:     Host = "localhost",
  125:     Port = ?PORT,
  126:     {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, raw},
  127:                                                 {active, false}]),
  128:     ok = gen_tcp:send(Socket,
  129:                       ["GET /ws-xmpp HTTP/1.1\r\n"
  130:                        "Host: localhost:5280\r\n"
  131:                        "Connection: upgrade\r\n"
  132:                        "Origin: http://localhost\r\n"
  133:                        "Sec-WebSocket-Key: NT1P6NvEFQyDDKuTyEN+1Q==\r\n"
  134:                        "Sec-WebSocket-Version: 13\r\n"
  135:                        "Upgrade: websocket\r\n"
  136:                        ++ maps:get(extra_headers, Opts, ""),
  137:                        "\r\n"]),
  138:     {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000),
  139:     Packet = erlang:decode_packet(http, Handshake, []),
  140:     {ok, {http_response, {1,1}, 101, "Switching Protocols"}, Rest} = Packet,
  141:     {Headers, _} = consume_headers(Rest, []),
  142:     InternalSocket = get_websocket(),
  143:     #{socket => Socket, internal_socket => InternalSocket, headers => Headers}.
  144: 
  145: consume_headers(Data, Headers) ->
  146:     case erlang:decode_packet(httph, Data, []) of
  147:         {ok, http_eoh, Rest} ->
  148:             {lists:reverse(Headers), Rest};
  149:         {ok, {http_header,_,Name,_,Value}, Rest} ->
  150:             consume_headers(Rest, [{Name, Value}|Headers])
  151:     end.
  152: 
  153: wait_for_ping(_, Try, _) when Try > 10 ->
  154:     {error, no_ping_packet};
  155: wait_for_ping(Socket, Try, Timeout) ->
  156:     {Reply, Content} = gen_tcp:recv(Socket, 0, Timeout),
  157:     case Reply of
  158:         error ->
  159:             {error, Content};
  160:         ok ->
  161:             Ping = ws_rx_frame(<<"">>, 9),
  162:             case Content of
  163:                 Ping ->
  164:                     ok;
  165:                 _ ->
  166:                     wait_for_ping(Socket, Try + 1, Timeout)
  167:             end
  168:     end.
  169: 
  170: %% Helpers
  171: ws_rx_frame(Payload, Opcode) ->
  172:     Length = byte_size(Payload),
  173:     <<1:1, 0:3, Opcode:4, 0:1, Length:7, Payload/binary>>.
  174: 
  175: get_websocket() ->
  176:     %% Assumption: there's only one ranch protocol process running and
  177:     %% it's the one which started due to our gen_tcp:connect in ws_handshake/1
  178:     [{cowboy_clear, Pid}] = get_ranch_connections(),
  179:     %% This is a record! See mod_websockets: #websocket{}.
  180:     {websocket, Pid, fake_peername, undefined}.
  181: 
  182: get_child_by_mod(Sup, Mod) ->
  183:     Kids = supervisor:which_children(Sup),
  184:     case lists:keyfind([Mod], 4, Kids) of
  185:         false -> error(not_found, [Sup, Mod]);
  186:         {_, KidPid, _, _} -> KidPid
  187:     end.
  188: 
  189: get_ranch_connections() ->
  190:     LSup = get_child_by_mod(ranch_sup, ranch_listener_sup),
  191:     CSup = get_child_by_mod(LSup, ranch_conns_sup_sup),
  192:     [{Mod, Pid} || {_, Sup, _, [ranch_conns_sup]} <- supervisor:which_children(CSup),
  193:                    {_, Pid, _, [Mod]} <- supervisor:which_children(Sup)].
  194: 
  195: wait_for_no_ranch_connections(Times) ->
  196:     case get_ranch_connections() of
  197:         [] ->
  198:             ok;
  199:         _ when Times > 0 ->
  200:             timer:sleep(100),
  201:             wait_for_no_ranch_connections(Times - 1);
  202:        Connections ->
  203:             error(#{reason => wait_for_no_ranch_connections_failed,
  204:                     connections => Connections})
  205:     end.
  206: 
  207: %% ---------------------------------------------------------------------
  208: %% subprotocol_header_tests test functions
  209: %% ---------------------------------------------------------------------
  210: 
  211: %% From RFC 6455:
  212: %%   The |Sec-WebSocket-Protocol| header field is used in the WebSocket
  213: %%   opening handshake.  It is sent from the client to the server and back
  214: %%   from the server to the client to confirm the subprotocol of the
  215: %%   connection.
  216: agree_to_xmpp_subprotocol(_) ->
  217:     check_subprotocol("Proper client behaviour", ["xmpp"], "xmpp").
  218: 
  219: agree_to_xmpp_subprotocol_case_insensitive(_) ->
  220:     %% The value must conform to the requirements
  221:     %% given in item 10 of Section 4.1 of this specification -- namely,
  222:     %% the value must be a token as defined by RFC 2616 [RFC2616].
  223:     %% ...
  224:     %% 10.  The request MAY include a header field with the name
  225:     %%      |Sec-WebSocket-Protocol|.  If present, this value indicates one
  226:     %%      or more comma-separated subprotocol the client wishes to speak,
  227:     %%      ordered by preference.  The elements that comprise this value
  228:     %%      MUST be non-empty strings with characters in the range U+0021 to
  229:     %%      U+007E not including separator characters as defined in
  230:     %%      [RFC2616] and MUST all be unique strings.  The ABNF for the
  231:     %%      value of this header field is 1#token, where the definitions of
  232:     %%      constructs and rules are as given in [RFC2616].
  233:     %% ...
  234:     check_subprotocol("Case insensitive", ["XMPP"], "XMPP").
  235: 
  236: %% From RFC 6455:
  237: %%   The |Sec-WebSocket-Protocol| header field MAY appear multiple times
  238: %%   in an HTTP request (which is logically the same as a single
  239: %%   |Sec-WebSocket-Protocol| header field that contains all values).
  240: %%   However, the |Sec-WebSocket-Protocol| header field MUST NOT appear
  241: %%   more than once in an HTTP response.
  242: agree_to_xmpp_subprotocol_from_many(_) ->
  243:     check_subprotocol("Two protocols in one header", ["xmpp, other"], "xmpp"),
  244:     check_subprotocol("Two protocols in one header", ["other, xmpp"], "xmpp"),
  245:     check_subprotocol("Two protocols in two headers", ["other", "xmpp"], "xmpp").
  246: 
  247: %% Do not set a Sec-Websocket-Protocol header in response if it's missing in a request.
  248: %%
  249: %% From RFC 6455:
  250: %%   if the server does not wish to agree to one of the suggested
  251: %%   subprotocols, it MUST NOT send back a |Sec-WebSocket-Protocol|
  252: %%   header field in its response.
  253: do_not_agree_to_missing_subprotocol(_) ->
  254:     check_subprotocol("Subprotocol header is missing", [], undefined).
  255: 
  256: %% Do not set a Sec-Websocket-Protocol header in response if it's provided, but not xmpp.
  257: do_not_agree_to_other_subprotocol(_) ->
  258:     check_subprotocol("Subprotocol is not xmpp", ["other"], undefined).
  259: 
  260: 
  261: %% ---------------------------------------------------------------------
  262: %% subprotocol_header_tests helper functions
  263: %% ---------------------------------------------------------------------
  264: 
  265: check_subprotocol(Comment, ProtoList, ExpectedProtocol) ->
  266:     ReqHeaders = lists:append(["Sec-Websocket-Protocol: " ++ Proto ++ "\r\n" || Proto <- ProtoList]),
  267:     Info = #{reason => check_subprotocol_failed,
  268:              comment => Comment,
  269:              expected_protocol => ExpectedProtocol,
  270:              request_headers => ReqHeaders},
  271:     #{headers := RespHeaders, socket := Socket} = ws_handshake(#{extra_headers => ReqHeaders}),
  272:     %% get_websocket/0 does not support more than one open connection
  273:     gen_tcp:close(Socket),
  274:     wait_for_no_ranch_connections(10),
  275:     RespProtocol = proplists:get_value("Sec-Websocket-Protocol", RespHeaders),
  276:     case RespProtocol of
  277:         ExpectedProtocol ->
  278:             ok;
  279:         _ ->
  280:             Info2 = Info#{response_headers => RespHeaders,
  281:                           response_protocol => RespProtocol},
  282:             ct:fail(Info2)
  283:     end.