1: -module(mongoose_listener_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include_lib("eunit/include/eunit.hrl").
    6: 
    7: -import(ejabberd_helper, [copy/2,
    8:                           data/2]).
    9: 
   10: -define(DEFAULT_PORTS, [5222, 5280, 5269]).
   11: 
   12: all() ->
   13:     [tcp_socket_is_started_with_default_backlog,
   14:      tcp_socket_is_started_with_options,
   15:      tcp_socket_supports_proxy_protocol,
   16:      tcp_socket_has_connection_details,
   17:      tcp_socket_supports_proxy_protocol,
   18:      tcp_start_stop_reload
   19:      ].
   20: 
   21: init_per_testcase(_Case, Config) ->
   22:     meck:new([gen_tcp], [unstick, passthrough]),
   23:     meck:expect(gen_tcp, listen,
   24:                 fun(Port, Opts) -> meck:passthrough([Port, Opts]) end),
   25:     Config.
   26: 
   27: end_per_testcase(_Case, Config) ->
   28:     meck:unload(),
   29:     Config.
   30: 
   31: init_per_suite(C) ->
   32:     mnesia:start(),
   33:     C.
   34: 
   35: end_per_suite(_C) ->
   36:     mnesia:stop(),
   37:     mnesia:delete_schema([node()]).
   38: 
   39: tcp_socket_is_started_with_default_backlog(_C) ->
   40:     ok = listener_started(#{}),
   41:     [{_Pid, {gen_tcp, listen, [_, Opts]}, _Result}] =  meck:history(gen_tcp),
   42:     100 = proplists:get_value(backlog, Opts).
   43: 
   44: tcp_socket_is_started_with_options(_C) ->
   45:     ok = listener_started(#{backlog => 50}),
   46:     [{_Pid, {gen_tcp, listen, [_, Opts]}, _Result}] =  meck:history(gen_tcp),
   47:     50 = proplists:get_value(backlog, Opts).
   48: 
   49: tcp_socket_has_connection_details(_C) ->
   50:     ok = listener_started(#{}),
   51:     {Port, _, _} = tcp_port_ip(),
   52: 
   53:     meck:new(mongoose_transport),
   54:     TestPid = self(),
   55:     meck:expect(mongoose_transport, accept,
   56:                 fun(_Module, Socket, Opts, ConnectionDetails) ->
   57:                         TestPid ! {socket_started, Socket, Opts, ConnectionDetails},
   58:                         ok
   59:                 end),
   60: 
   61:     {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]),
   62:     {ok, SrcPort} = inet:port(Socket),
   63: 
   64:     receive
   65:         {socket_started, _Socket, _Opts, ConnectionDetails} ->
   66:             ?assertEqual(#{proxy => false,
   67:                            src_address => {127, 0, 0, 1},
   68:                            src_port => SrcPort,
   69:                            dest_address => {127, 0, 0, 1},
   70:                            dest_port => Port}, ConnectionDetails)
   71:     after
   72:         5000 ->
   73:             ct:fail(timeout_waiting_for_tcp)
   74:     end.
   75: 
   76: tcp_socket_supports_proxy_protocol(_C) ->
   77:     ok = listener_started(#{proxy_protocol => true}),
   78: 
   79:     CommonProxyInfo = #{src_address => {1, 2, 3, 4},
   80:                         src_port => 444,
   81:                         dest_address => {192, 168, 0, 1},
   82:                         dest_port => 443,
   83:                         version => 2},
   84:     RanchProxyInfo = CommonProxyInfo#{command => proxy,
   85:                                       transport_family => ipv4,
   86:                                       transport_protocol => stream},
   87:     {Port, _, _} = tcp_port_ip(),
   88: 
   89:     meck:new(mongoose_transport),
   90:     TestPid = self(),
   91:     meck:expect(mongoose_transport, accept,
   92:                 fun(_Module, Socket, Opts, ConnectionDetails) ->
   93:                         TestPid ! {socket_started, Socket, Opts, ConnectionDetails},
   94:                         ok
   95:                 end),
   96: 
   97:     {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]),
   98:     ok = gen_tcp:send(Socket, [ranch_proxy_header:header(RanchProxyInfo)]),
   99: 
  100:     receive
  101:         {socket_started, _Socket, _Opts, ConnectionDetails} ->
  102:             ?assertEqual(CommonProxyInfo#{proxy => true}, ConnectionDetails)
  103:     after
  104:         5000 ->
  105:             ct:fail(timeout_waiting_for_tcp_with_proxy_protocol)
  106:     end.
  107: 
  108: listener_started(Opts) ->
  109:     mim_ct_sup:start_link(ejabberd_sup),
  110:     mongoose_listener_sup:start_link(),
  111:     mongoose_listener:start_listener(maps:merge(listener_opts(), Opts)).
  112: 
  113: tcp_port_ip() ->
  114:     {1805, {0, 0, 0, 0}, tcp}.
  115: 
  116: listener_opts() ->
  117:     #{module => ?MODULE,
  118:       port => 1805,
  119:       ip_tuple => {0, 0, 0, 0},
  120:       ip_address => "0",
  121:       ip_version => 4,
  122:       proto => tcp,
  123:       num_acceptors => 1,
  124:       backlog => 100,
  125:       proxy_protocol => false}.
  126: 
  127: tcp_start_stop_reload(C) ->
  128:     %% start server
  129:     copy(data(C, "mongooseim.basic.toml"), data(C, "mongooseim.toml")),
  130:     ejabberd_helper:start_ejabberd_with_config(C, "mongooseim.toml"),
  131:     ?assert(lists:keymember(mongooseim, 1, application:which_applications())),
  132:     %% make sure all ports are open
  133:     lists:map(fun assert_open/1, ?DEFAULT_PORTS),
  134:     %% stop listeners, now they should be closed
  135:     Listeners = mongoose_config:get_opt(listen),
  136:     lists:foreach(fun mongoose_listener:stop_listener/1, Listeners),
  137:     lists:map(fun assert_closed/1, ?DEFAULT_PORTS),
  138:     %% and start them all again
  139:     lists:foreach(fun mongoose_listener:start_listener/1, Listeners),
  140:     lists:map(fun assert_open/1, ?DEFAULT_PORTS),
  141: 
  142:     %% we want to make sure that connection to an unchanged port survives reload
  143:     UnchPort = 5222,
  144:     {ok, Sock} = gen_tcp:connect("localhost", UnchPort,[{active, false}, {packet, 2}]),
  145:     assert_connected(Sock, UnchPort),
  146: 
  147:     %% and that to the changed port does too (this is current implementation)
  148:     ChgPort = 5269,
  149:     {ok, Sock2} = gen_tcp:connect("localhost", ChgPort,[{active, false}, {packet, 2}]),
  150:     assert_connected(Sock2, ChgPort),
  151: 
  152:     ok = mongooseim:stop(),
  153:     ok.
  154: 
  155: assert_open(PortNo) ->
  156:     case gen_tcp:connect("localhost", PortNo, [{active, false}, {packet, 2}]) of
  157:         {ok, Socket} ->
  158:             gen_tcp:close(Socket),
  159:             ok;
  160:         E ->
  161:             ct:fail("Failed: port ~p is closed, should be open; error was: ~p", [PortNo, E])
  162:     end .
  163: 
  164: assert_closed(PortNo) ->
  165:     F = fun() ->
  166:               gen_tcp:connect("localhost", PortNo, [{active, false}, {packet, 2}])
  167:         end,
  168:     async_helper:wait_until(F, {error, econnrefused}).
  169: 
  170: 
  171: assert_connected(Sock, Port) ->
  172:     case gen_tcp:recv(Sock, 0, 500) of
  173:         {error, timeout} ->
  174:             ok;
  175:         Else ->
  176:             ct:fail("Failed: connection to ~p is broken, error was: ~p", [Port, Else])
  177:     end.
  178: 
  179: start_listener(Opts) ->
  180:     mongoose_tcp_listener:start_listener(Opts).