1: %%%===================================================================
    2: %%% @copyright (C) 2012, Erlang Solutions Ltd.
    3: %%% @doc Suite for testing s2s connection
    4: %%% @end
    5: %%%===================================================================
    6: 
    7: -module(s2s_SUITE).
    8: -compile([export_all, nowarn_export_all]).
    9: 
   10: -include_lib("escalus/include/escalus.hrl").
   11: -include_lib("exml/include/exml.hrl").
   12: -include_lib("exml/include/exml_stream.hrl").
   13: -include_lib("eunit/include/eunit.hrl").
   14: 
   15: %% Module aliases
   16: -define(dh, distributed_helper).
   17: -import(distributed_helper, [mim/0, rpc_spec/1, rpc/4]).
   18: 
   19: %%%===================================================================
   20: %%% Suite configuration
   21: %%%===================================================================
   22: 
   23: all() ->
   24:     [
   25:      {group, both_plain},
   26:      {group, both_tls_optional}, %% default MongooseIM config
   27:      {group, both_tls_required},
   28: 
   29:      {group, node1_tls_optional_node2_tls_required},
   30:      {group, node1_tls_required_node2_tls_optional},
   31: 
   32:      {group, node1_tls_required_trusted_node2_tls_optional},
   33:      {group, node1_tls_optional_node2_tls_required_trusted_with_cachain},
   34: 
   35:      {group, node1_tls_false_node2_tls_optional},
   36:      {group, node1_tls_optional_node2_tls_false},
   37: 
   38:      {group, node1_tls_false_node2_tls_required},
   39:      {group, node1_tls_required_node2_tls_false},
   40: 
   41:      {group, dialback}
   42:     ].
   43: 
   44: groups() ->
   45:     [{both_plain, [sequence], all_tests()},
   46:      {both_tls_optional, [], essentials()},
   47:      {both_tls_required, [], essentials()},
   48: 
   49:      {node1_tls_optional_node2_tls_required, [], essentials()},
   50:      {node1_tls_required_node2_tls_optional, [], essentials()},
   51: 
   52:      %% Node1 closes connection from nodes with invalid certs
   53:      {node1_tls_required_trusted_node2_tls_optional, [], negative()},
   54: 
   55:      %% Node1 accepts connection provided the cert can be verified
   56:      {node1_tls_optional_node2_tls_required_trusted_with_cachain, [parallel],
   57:       essentials() ++ connection_cases()},
   58: 
   59:      {node1_tls_false_node2_tls_optional, [], essentials()},
   60:      {node1_tls_optional_node2_tls_false, [], essentials()},
   61: 
   62:      {node1_tls_false_node2_tls_required, [], negative()},
   63:      {node1_tls_required_node2_tls_false, [], negative()},
   64:      {dialback, [], [dialback_key_is_synchronized_on_different_nodes]}].
   65: 
   66: essentials() ->
   67:     [simple_message].
   68: 
   69: all_tests() ->
   70:     [connections_info, nonexistent_user, unknown_domain, malformed_jid,
   71:      dialback_with_wrong_key | essentials()].
   72: 
   73: negative() ->
   74:     [timeout_waiting_for_message].
   75: 
   76: connection_cases() ->
   77:     [successful_external_auth_with_valid_cert,
   78:      start_stream_fails_for_wrong_namespace,
   79:      start_stream_fails_for_wrong_version,
   80:      start_stream_fails_without_version,
   81:      start_stream_fails_without_host,
   82:      start_stream_fails_for_unknown_host,
   83:      starttls_fails_for_unknown_host,
   84:      only_messages_from_authenticated_domain_users_are_accepted,
   85:      auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert,
   86:      auth_with_valid_cert_fails_for_other_mechanism_than_external].
   87: 
   88: suite() ->
   89:     distributed_helper:require_rpc_nodes([mim, mim2, fed]) ++ escalus:suite().
   90: 
   91: users() ->
   92:     [alice2, alice, bob].
   93: 
   94: %%%===================================================================
   95: %%% Init & teardown
   96: %%%===================================================================
   97: 
   98: init_per_suite(Config) ->
   99:     Config1 = s2s_helper:init_s2s(escalus:init_per_suite(Config)),
  100:     escalus:create_users(Config1, escalus:get_users(users())).
  101: 
  102: end_per_suite(Config) ->
  103:     escalus_fresh:clean(),
  104:     s2s_helper:end_s2s(Config),
  105:     escalus:delete_users(Config, escalus:get_users(users())),
  106:     escalus:end_per_suite(Config).
  107: 
  108: init_per_group(dialback, Config) ->
  109:     %% Tell mnesia that mim and mim2 nodes are clustered
  110:     distributed_helper:add_node_to_cluster(distributed_helper:mim2(), Config);
  111: init_per_group(GroupName, Config) ->
  112:     s2s_helper:configure_s2s(GroupName, Config).
  113: 
  114: end_per_group(_GroupName, _Config) ->
  115:     ok.
  116: 
  117: init_per_testcase(CaseName, Config) ->
  118:     escalus:init_per_testcase(CaseName, Config).
  119: 
  120: end_per_testcase(CaseName, Config) ->
  121:     escalus:end_per_testcase(CaseName, Config).
  122: 
  123: %%%===================================================================
  124: %%% Server-to-server communication test
  125: %%%===================================================================
  126: 
  127: simple_message(Config) ->
  128:     %% check that metrics are bounced
  129:     MongooseMetrics = [{[global, data, xmpp, received, s2s], changed},
  130:                        {[global, data, xmpp, sent, s2s], changed}],
  131:     escalus:fresh_story([{mongoose_metrics, MongooseMetrics} | Config],
  132:                         [{alice2, 1}, {alice, 1}], fun(Alice2, Alice1) ->
  133: 
  134:         %% User on the main server sends a message to a user on a federated server
  135:         escalus:send(Alice1, escalus_stanza:chat_to(Alice2, <<"Hi, foreign Alice!">>)),
  136: 
  137:         %% User on the federated server receives the message
  138:         Stanza = escalus:wait_for_stanza(Alice2, 10000),
  139:         escalus:assert(is_chat_message, [<<"Hi, foreign Alice!">>], Stanza),
  140: 
  141:         %% User on the federated server sends a message to the main server
  142:         escalus:send(Alice2, escalus_stanza:chat_to(Alice1, <<"Nice to meet you!">>)),
  143: 
  144:         %% User on the main server receives the message
  145:         Stanza2 = escalus:wait_for_stanza(Alice1, 10000),
  146:         escalus:assert(is_chat_message, [<<"Nice to meet you!">>], Stanza2)
  147: 
  148:     end).
  149: 
  150: timeout_waiting_for_message(Config) ->
  151:     try
  152:         simple_message(Config),
  153:         ct:fail("got message but shouldn't")
  154:     catch
  155:         error:timeout_when_waiting_for_stanza ->
  156:             ok
  157:     end.
  158: 
  159: connections_info(Config) ->
  160:     simple_message(Config),
  161:     FedDomain = ct:get_config({hosts, fed, domain}),
  162:     %% there should be at least one in and at least one out connection
  163:     [_ | _] = get_s2s_connections(?dh:mim(), FedDomain, in),
  164:     [_ | _] = get_s2s_connections(?dh:mim(), FedDomain, out),
  165:     ok.
  166: 
  167: get_s2s_connections(RPCSpec, Domain, Type) ->
  168:     AllS2SConnections = ?dh:rpc(RPCSpec, mongoose_s2s_info, get_connections, [Type]),
  169:     DomainS2SConnections = 
  170:         [Connection || Connection <- AllS2SConnections,
  171:                        Type =/= in orelse [Domain] =:= maps:get(domains, Connection),
  172:                        Type =/= out orelse Domain =:= maps:get(server, Connection)],
  173:     ct:pal("Node = ~p,  ConnectionType = ~p, Domain = ~s~nDomainS2SConnections(~p): ~p",
  174:            [maps:get(node, RPCSpec), Type, Domain, length(DomainS2SConnections),
  175:             DomainS2SConnections]),
  176:     DomainS2SConnections.
  177: 
  178: nonexistent_user(Config) ->
  179:     escalus:fresh_story(Config, [{alice, 1}, {alice2, 1}], fun(Alice1, Alice2) ->
  180: 
  181:         %% Alice@localhost1 sends message to Xyz@localhost2
  182:         RemoteServer = escalus_client:server(Alice2),
  183:         Fake = <<"xyz@", RemoteServer/binary>>,
  184:         escalus:send(Alice1, escalus_stanza:chat_to(Fake,
  185:                                                     <<"Hello, nonexistent!">>)),
  186: 
  187:         %% Alice@localhost1 receives stanza error: service-unavailable
  188:         Stanza = escalus:wait_for_stanza(Alice1),
  189:         escalus:assert(is_error, [<<"cancel">>, <<"service-unavailable">>], Stanza)
  190: 
  191:     end).
  192: 
  193: unknown_domain(Config) ->
  194:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) ->
  195: 
  196:         %% Alice@localhost1 sends message to Xyz@localhost3
  197:         escalus:send(Alice1, escalus_stanza:chat_to(
  198:             <<"xyz@somebogushost">>,
  199:             <<"Hello, unreachable!">>)),
  200: 
  201:         %% Alice@localhost1 receives stanza error: remote-server-not-found
  202:         Stanza = escalus:wait_for_stanza(Alice1, 10000),
  203:         escalus:assert(is_error, [<<"cancel">>, <<"remote-server-not-found">>], Stanza)
  204: 
  205:     end).
  206: 
  207: malformed_jid(Config) ->
  208:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) ->
  209: 
  210:         %% Alice@localhost1 sends message to Xyz@localhost3
  211:         escalus:send(Alice1, escalus_stanza:chat_to(
  212:             <<"not a jid">>,
  213:             <<"Hello, unreachable!">>)),
  214: 
  215:         %% Alice@localhost1 receives stanza error: remote-server-not-found
  216:         Stanza = escalus:wait_for_stanza(Alice1, 10000),
  217:         escalus:assert(is_error, [<<"cancel">>, <<"remote-server-not-found">>], Stanza)
  218: 
  219:     end).
  220: 
  221: dialback_with_wrong_key(_Config) ->
  222:     HostType = domain_helper:host_type(mim),
  223:     MimDomain = domain_helper:domain(mim),
  224:     FedDomain = domain_helper:domain(fed),
  225:     FromTo = {MimDomain, FedDomain},
  226:     Key = <<"123456">>, %% wrong key
  227:     StreamId = <<"sdfdsferrr">>,
  228:     StartType = {verify, self(), Key, StreamId},
  229:     {ok, _} = rpc(rpc_spec(mim), ejabberd_s2s_out, start, [FromTo, StartType]),
  230:     receive
  231:         %% Remote server (fed1) rejected out request
  232:         {'$gen_event', {validity_from_s2s_out, false, FromTo}} ->
  233:             ok
  234:         after 5000 ->
  235:             ct:fail(timeout)
  236:     end.
  237: 
  238: nonascii_addr(Config) ->
  239:     escalus:fresh_story(Config, [{alice, 1}, {bob2, 1}], fun(Alice, Bob) ->
  240: 
  241:         %% Bob@localhost2 sends message to Alice@localhost1
  242:         escalus:send(Bob, escalus_stanza:chat_to(Alice, <<"Cześć Alice!">>)),
  243: 
  244:         %% Alice@localhost1 receives message from Bob@localhost2
  245:         Stanza = escalus:wait_for_stanza(Alice, 10000),
  246:         escalus:assert(is_chat_message, [<<"Cześć Alice!">>], Stanza),
  247: 
  248:         %% Alice@localhost1 sends message to Bob@localhost2
  249:         escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"Miło Cię poznać">>)),
  250: 
  251:         %% Bob@localhost2 receives message from Alice@localhost1
  252:         Stanza2 = escalus:wait_for_stanza(Bob, 10000),
  253:         escalus:assert(is_chat_message, [<<"Miło Cię poznać">>], Stanza2)
  254: 
  255:     end).
  256: 
  257: successful_external_auth_with_valid_cert(Config) ->
  258:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  259:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  260:                                                        [fun s2s_start_stream/2,
  261:                                                         fun s2s_starttls/2,
  262:                                                         fun s2s_external_auth/2]),
  263:     escalus_connection:stop(Client).
  264: 
  265: start_stream_fails_for_wrong_namespace(Config) ->
  266:     start_stream_fails(Config, <<"invalid-namespace">>,
  267:                        [fun s2s_start_stream_with_wrong_namespace/2]).
  268: 
  269: start_stream_fails_for_wrong_version(Config) ->
  270:     %% TLS authentication requires version 1.0
  271:     start_stream_fails(Config, <<"invalid-xml">>,
  272:                        [fun s2s_start_stream_with_wrong_version/2]).
  273: 
  274: start_stream_fails_without_version(Config) ->
  275:     %% TLS authentication requires version 1.0
  276:     start_stream_fails(Config, <<"invalid-xml">>,
  277:                        [fun s2s_start_stream_without_version/2]).
  278: 
  279: start_stream_fails_without_host(Config) ->
  280:     start_stream_fails(Config, <<"improper-addressing">>,
  281:                        [fun s2s_start_stream_without_host/2]).
  282: 
  283: start_stream_fails_for_unknown_host(Config) ->
  284:     start_stream_fails(Config, <<"host-unknown">>,
  285:                        [fun s2s_start_stream_to_wrong_host/2]).
  286: 
  287: starttls_fails_for_unknown_host(Config) ->
  288:     start_stream_fails(Config, <<"host-unknown">>,
  289:                        [fun s2s_start_stream/2,
  290:                         fun s2s_starttls_to_wrong_host/2]).
  291: 
  292: start_stream_fails(Config, ErrorType, ConnectionSteps) ->
  293:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  294:     {ok, Client, _} = escalus_connection:start(ConnectionArgs, ConnectionSteps),
  295:     [Start, Error, End] = escalus:wait_for_stanzas(Client, 3),
  296:     escalus:assert(is_stream_start, Start),
  297:     escalus:assert(is_stream_error, [ErrorType, <<>>], Error),
  298:     escalus:assert(is_stream_end, End).
  299: 
  300: only_messages_from_authenticated_domain_users_are_accepted(Config) ->
  301:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  302:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  303:                                                        [fun s2s_start_stream/2,
  304:                                                         fun s2s_starttls/2,
  305:                                                         fun s2s_external_auth/2]),
  306:     escalus:fresh_story(Config, [{alice2, 1}], fun(Alice) ->
  307: 
  308:         UserInWrongDomain = <<"a_user@this_is_not_my.domain.com">>,
  309:         ChatToAliceFromUserInWrongDomain = escalus_stanza:chat(UserInWrongDomain,
  310:                                                                Alice, <<"Miło Cię poznać">>),
  311:         %% Client is a s2s connection established and authenticated for domain "localhost"
  312:         %% Now we try to send a message from other domain than "localhost"
  313:         %% over the established s2s connection
  314:         escalus:send(Client, ChatToAliceFromUserInWrongDomain),
  315: 
  316:         %% Alice@fed1 does not receives message from a_user@this_is_not_my.domain.com
  317:         timer:sleep(timer:seconds(5)),
  318:         escalus_assert:has_no_stanzas(Alice)
  319: 
  320:     end),
  321: 
  322:     escalus_connection:stop(Client).
  323: 
  324: auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert(Config) ->
  325:     ConnectionArgs = connection_args("not_in_cert_domain", <<"not_in_cert_domain">>, Config),
  326:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  327:                                                        [fun s2s_start_stream/2,
  328:                                                         fun s2s_starttls/2]),
  329: 
  330:     try
  331:         escalus_auth:auth_sasl_external(Client, Client#client.props),
  332:         ct:fail("Authenitcated but MUST NOT")
  333:     catch throw:{auth_failed, _, _} ->
  334:               escalus_connection:wait_for_close(Client, timer:seconds(5))
  335:     end.
  336: 
  337: auth_with_valid_cert_fails_for_other_mechanism_than_external(Config) ->
  338:     ConnectionArgs = connection_args("localhost", <<"localhost">>, Config),
  339:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  340:                                                        [fun s2s_start_stream/2,
  341:                                                         fun s2s_starttls/2
  342:                                                        ]),
  343: 
  344:     Stanza = escalus_stanza:auth(<<"ANONYMOUS">>),
  345:     ok = escalus_connection:send(Client, Stanza),
  346:     #xmlel{name = <<"failure">>} = escalus_connection:get_stanza(Client, wait_for_auth_reply),
  347: 
  348:     escalus_connection:wait_for_close(Client, timer:seconds(5)).
  349: 
  350: connection_args(FromServer, RequestedName, Config) ->
  351:     {KeyFile, CertFile} = get_main_key_and_cert_files(Config),
  352:     [{host, "localhost"},
  353:      {to_server, "fed1"},
  354:      {from_server, FromServer},
  355:      {requested_name, RequestedName},
  356:      {starttls, required},
  357:      {port, ct:get_config({hosts, fed, incoming_s2s_port})},
  358:      {ssl_opts, [{versions, ['tlsv1.2']}, {verify, verify_none}, {certfile, CertFile}, {keyfile, KeyFile}]}].
  359: 
  360: s2s_start_stream_with_wrong_namespace(Conn = #client{props = Props}, Features) ->
  361:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"xmlns">> => <<"42">>} end),
  362:     ok = escalus_connection:send(Conn, Start),
  363:     {Conn, Features}.
  364: 
  365: s2s_start_stream_with_wrong_version(Conn = #client{props = Props}, Features) ->
  366:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"version">> => <<"42">>} end),
  367:     ok = escalus_connection:send(Conn, Start),
  368:     {Conn, Features}.
  369: 
  370: s2s_start_stream_without_version(Conn = #client{props = Props}, Features) ->
  371:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"version">>, Attrs) end),
  372:     ok = escalus_connection:send(Conn, Start),
  373:     {Conn, Features}.
  374: 
  375: s2s_start_stream_without_host(Conn = #client{props = Props}, Features) ->
  376:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"to">>, Attrs) end),
  377:     ok = escalus_connection:send(Conn, Start),
  378:     {Conn, Features}.
  379: 
  380: s2s_start_stream_to_wrong_host(Conn = #client{props = Props}, Features) ->
  381:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"to">> => <<"42">>} end),
  382:     ok = escalus_connection:send(Conn, Start),
  383:     {Conn, Features}.
  384: 
  385: s2s_start_stream(Conn = #client{props = Props}, []) ->
  386:     StreamStartRep = s2s_start_stream_and_wait_for_response(Conn),
  387: 
  388:     #xmlstreamstart{attrs = Attrs} = StreamStartRep,
  389:     Id = proplists:get_value(<<"id">>, Attrs),
  390: 
  391:     escalus_session:stream_features(Conn#client{props = [{sid, Id} | Props]}, []).
  392: 
  393: s2s_start_stream_and_wait_for_response(Conn = #client{props = Props}) ->
  394:     StreamStart = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs end),
  395:     ok = escalus_connection:send(Conn, StreamStart),
  396:     escalus_connection:get_stanza(Conn, wait_for_stream).
  397: 
  398: s2s_stream_start_stanza(Props, F) ->
  399:     Attrs = (stream_start_attrs())#{<<"to">> => proplists:get_value(to_server, Props),
  400:                                     <<"from">> => proplists:get_value(from_server, Props)},
  401:     #xmlstreamstart{name = <<"stream:stream">>, attrs = maps:to_list(F(Attrs))}.
  402: 
  403: stream_start_attrs() ->
  404:     #{<<"xmlns">> => <<"jabber:server">>,
  405:       <<"xmlns:stream">> => <<"http://etherx.jabber.org/streams">>,
  406:       <<"version">> => <<"1.0">>}.
  407: 
  408: s2s_starttls(Client, Features, StartStreamF) ->
  409:     case proplists:get_value(starttls, Features) of
  410:         false ->
  411:             ct:fail("The server does not offer STARTTLS");
  412:         _ ->
  413:             ok
  414:     end,
  415: 
  416:     escalus_connection:send(Client, escalus_stanza:starttls()),
  417:     escalus_connection:get_stanza(Client, proceed),
  418:     escalus_connection:upgrade_to_tls(Client),
  419:     StartStreamF(Client, []).
  420: 
  421: s2s_starttls(Client, Features) ->
  422:     s2s_starttls(Client, Features, fun s2s_start_stream/2).
  423: 
  424: s2s_starttls_to_wrong_host(Client, Features) ->
  425:     s2s_starttls(Client, Features, fun s2s_start_stream_to_wrong_host/2).
  426: 
  427: s2s_external_auth(Client = #client{props = Props}, Features) ->
  428:     case proplists:get_value(sasl_mechanisms, Features) of
  429:         [<<"EXTERNAL">>] ->
  430:             ok;
  431:         SASL ->
  432:             ct:fail("Server does not provide EXTERNAL auth: ~p", [SASL])
  433:     end,
  434:     escalus_auth:auth_sasl_external(Client, Props),
  435:     s2s_start_stream(Client, []).
  436: 
  437: get_main_key_and_cert_files(Config) ->
  438:     CertFile = get_main_file_path(Config, "cert.pem"),
  439:     KeyFile = get_main_file_path(Config, "key.pem"),
  440:     {KeyFile, CertFile}.
  441: 
  442: get_main_file_path(Config, File) ->
  443:     filename:join([path_helper:repo_dir(Config),
  444:                    "tools", "ssl", "mongooseim", File]).
  445: 
  446: dialback_key_is_synchronized_on_different_nodes(_Config) ->
  447:     configure_secret_and_restart_s2s(mim),
  448:     configure_secret_and_restart_s2s(mim2),
  449:     Key1 = get_shared_secret(mim),
  450:     Key2 = get_shared_secret(mim2),
  451:     ?assertEqual(Key1, Key2),
  452:     %% Node 2 is restarted later, so both nodes should have the key.
  453:     ?assertEqual(Key2, {ok, <<"9e438f25e81cf347100b">>}).
  454: 
  455: get_shared_secret(NodeKey) ->
  456:     HostType = domain_helper:host_type(mim),
  457:     rpc(rpc_spec(NodeKey), mongoose_s2s_backend, get_shared_secret, [HostType]).
  458: 
  459: set_opt(Spec, Opt, Value) ->
  460:     rpc(Spec, mongoose_config, set_opt, [Opt, Value]).
  461: 
  462: configure_secret_and_restart_s2s(NodeKey) ->
  463:     HostType = domain_helper:host_type(mim),
  464:     Spec = rpc_spec(NodeKey),
  465:     set_opt(Spec, [{s2s, HostType}, shared], shared_secret(NodeKey)),
  466:     ok = rpc(Spec, supervisor, terminate_child, [ejabberd_sup, ejabberd_s2s]),
  467:     {ok, _} = rpc(Spec, supervisor, restart_child, [ejabberd_sup, ejabberd_s2s]).
  468: 
  469: shared_secret(mim) -> <<"f623e54a0741269be7dd">>; %% Some random key
  470: shared_secret(mim2) -> <<"9e438f25e81cf347100b">>.