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: 
   14: %% Module aliases
   15: -define(dh, distributed_helper).
   16: 
   17: %%%===================================================================
   18: %%% Suite configuration
   19: %%%===================================================================
   20: 
   21: all() ->
   22:     [
   23:      {group, both_plain},
   24:      {group, both_tls_optional}, %% default MongooseIM config
   25:      {group, both_tls_required},
   26: 
   27:      {group, node1_tls_optional_node2_tls_required},
   28:      {group, node1_tls_required_node2_tls_optional},
   29: 
   30:      {group, node1_tls_required_trusted_node2_tls_optional},
   31:      {group, node1_tls_optional_node2_tls_required_trusted_with_cachain},
   32: 
   33:      {group, node1_tls_false_node2_tls_optional},
   34:      {group, node1_tls_optional_node2_tls_false},
   35: 
   36:      {group, node1_tls_false_node2_tls_required},
   37:      {group, node1_tls_required_node2_tls_false}
   38: 
   39:     ].
   40: 
   41: groups() ->
   42:     [{both_plain, [sequence], all_tests()},
   43:      {both_tls_optional, [], essentials()},
   44:      {both_tls_required, [], essentials()},
   45: 
   46:      {node1_tls_optional_node2_tls_required, [], essentials()},
   47:      {node1_tls_required_node2_tls_optional, [], essentials()},
   48: 
   49:      %% Node1 closes connection from nodes with invalid certs
   50:      {node1_tls_required_trusted_node2_tls_optional, [], negative()},
   51: 
   52:      %% Node1 accepts connection provided the cert can be verified
   53:      {node1_tls_optional_node2_tls_required_trusted_with_cachain, [parallel],
   54:       essentials() ++ connection_cases()},
   55: 
   56:      {node1_tls_false_node2_tls_optional, [], essentials()},
   57:      {node1_tls_optional_node2_tls_false, [], essentials()},
   58: 
   59:      {node1_tls_false_node2_tls_required, [], negative()},
   60:      {node1_tls_required_node2_tls_false, [], negative()}].
   61: 
   62: essentials() ->
   63:     [simple_message].
   64: 
   65: all_tests() ->
   66:     essentials() ++ [connections_info, nonexistent_user, unknown_domain, malformed_jid].
   67: 
   68: negative() ->
   69:     [timeout_waiting_for_message].
   70: 
   71: connection_cases() ->
   72:     [successful_external_auth_with_valid_cert,
   73:      start_stream_fails_for_wrong_namespace,
   74:      start_stream_fails_for_wrong_version,
   75:      start_stream_fails_without_version,
   76:      start_stream_fails_without_host,
   77:      start_stream_fails_for_unknown_host,
   78:      starttls_fails_for_unknown_host,
   79:      only_messages_from_authenticated_domain_users_are_accepted,
   80:      auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert,
   81:      auth_with_valid_cert_fails_for_other_mechanism_than_external].
   82: 
   83: suite() ->
   84:     s2s_helper:suite(escalus:suite()).
   85: 
   86: users() ->
   87:     [alice2, alice, bob].
   88: 
   89: 
   90: 
   91: %%%===================================================================
   92: %%% Init & teardown
   93: %%%===================================================================
   94: 
   95: init_per_suite(Config) ->
   96:     Config1 = s2s_helper:init_s2s(escalus:init_per_suite(Config)),
   97:     escalus:create_users(Config1, escalus:get_users(users())).
   98: 
   99: end_per_suite(Config) ->
  100:     escalus_fresh:clean(),
  101:     s2s_helper:end_s2s(Config),
  102:     escalus:delete_users(Config, escalus:get_users(users())),
  103:     escalus:end_per_suite(Config).
  104: 
  105: init_per_group(GroupName, Config) ->
  106:     s2s_helper:configure_s2s(GroupName, Config).
  107: 
  108: end_per_group(_GroupName, _Config) ->
  109:     ok.
  110: 
  111: init_per_testcase(CaseName, Config) ->
  112:     escalus:init_per_testcase(CaseName, Config).
  113: 
  114: end_per_testcase(CaseName, Config) ->
  115:     escalus:end_per_testcase(CaseName, Config).
  116: 
  117: %%%===================================================================
  118: %%% Server-to-server communication test
  119: %%%===================================================================
  120: 
  121: simple_message(Config) ->
  122:     escalus:fresh_story(Config, [{alice2, 1}, {alice, 1}], fun(Alice2, Alice1) ->
  123: 
  124:         %% User on the main server sends a message to a user on a federated server
  125:         escalus:send(Alice1, escalus_stanza:chat_to(Alice2, <<"Hi, foreign Alice!">>)),
  126: 
  127:         %% User on the federated server receives the message
  128:         Stanza = escalus:wait_for_stanza(Alice2, 10000),
  129:         escalus:assert(is_chat_message, [<<"Hi, foreign Alice!">>], Stanza),
  130: 
  131:         %% User on the federated server sends a message to the main server
  132:         escalus:send(Alice2, escalus_stanza:chat_to(Alice1, <<"Nice to meet you!">>)),
  133: 
  134:         %% User on the main server receives the message
  135:         Stanza2 = escalus:wait_for_stanza(Alice1, 10000),
  136:         escalus:assert(is_chat_message, [<<"Nice to meet you!">>], Stanza2)
  137: 
  138:     end).
  139: 
  140: timeout_waiting_for_message(Config) ->
  141:     try
  142:         simple_message(Config),
  143:         ct:fail("got message but shouldn't")
  144:     catch
  145:         error:timeout_when_waiting_for_stanza ->
  146:             ok
  147:     end.
  148: 
  149: connections_info(Config) ->
  150:     simple_message(Config),
  151:     FedDomain = ct:get_config({hosts, fed, domain}),
  152:     S2SIn = ?dh:rpc(?dh:mim(), ejabberd_s2s, get_info_s2s_connections, [in]),
  153:     ct:pal("S2sIn: ~p", [S2SIn]),
  154:     true = lists:any(fun(PropList) -> [FedDomain] =:= proplists:get_value(domains, PropList) end,
  155:                      S2SIn),
  156:     S2SOut = ?dh:rpc(?dh:mim(), ejabberd_s2s, get_info_s2s_connections, [out]),
  157:     ct:pal("S2sOut: ~p", [S2SOut]),
  158:     true = lists:any(fun(PropList) -> FedDomain =:= proplists:get_value(server, PropList) end,
  159:                      S2SOut),
  160: 
  161:     ok.
  162: 
  163: 
  164: nonexistent_user(Config) ->
  165:     escalus:fresh_story(Config, [{alice, 1}, {alice2, 1}], fun(Alice1, Alice2) ->
  166: 
  167:         %% Alice@localhost1 sends message to Xyz@localhost2
  168:         RemoteServer = escalus_client:server(Alice2),
  169:         Fake = <<"xyz@", RemoteServer/binary>>,
  170:         escalus:send(Alice1, escalus_stanza:chat_to(Fake,
  171:                                                     <<"Hello, nonexistent!">>)),
  172: 
  173:         %% Alice@localhost1 receives stanza error: service-unavailable
  174:         Stanza = escalus:wait_for_stanza(Alice1),
  175:         escalus:assert(is_error, [<<"cancel">>, <<"service-unavailable">>], Stanza)
  176: 
  177:     end).
  178: 
  179: unknown_domain(Config) ->
  180:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) ->
  181: 
  182:         %% Alice@localhost1 sends message to Xyz@localhost3
  183:         escalus:send(Alice1, escalus_stanza:chat_to(
  184:             <<"xyz@somebogushost">>,
  185:             <<"Hello, unreachable!">>)),
  186: 
  187:         %% Alice@localhost1 receives stanza error: remote-server-not-found
  188:         Stanza = escalus:wait_for_stanza(Alice1, 10000),
  189:         escalus:assert(is_error, [<<"cancel">>, <<"remote-server-not-found">>], Stanza)
  190: 
  191:     end).
  192: 
  193: malformed_jid(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:             <<"not a jid">>,
  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: nonascii_addr(Config) ->
  208:     escalus:fresh_story(Config, [{alice, 1}, {bob2, 1}], fun(Alice, Bob) ->
  209: 
  210:         %% Bob@localhost2 sends message to Alice@localhost1
  211:         escalus:send(Bob, escalus_stanza:chat_to(Alice, <<"Cześć Alice!">>)),
  212: 
  213:         %% Alice@localhost1 receives message from Bob@localhost2
  214:         Stanza = escalus:wait_for_stanza(Alice, 10000),
  215:         escalus:assert(is_chat_message, [<<"Cześć Alice!">>], Stanza),
  216: 
  217:         %% Alice@localhost1 sends message to Bob@localhost2
  218:         escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"Miło Cię poznać">>)),
  219: 
  220:         %% Bob@localhost2 receives message from Alice@localhost1
  221:         Stanza2 = escalus:wait_for_stanza(Bob, 10000),
  222:         escalus:assert(is_chat_message, [<<"Miło Cię poznać">>], Stanza2)
  223: 
  224:     end).
  225: 
  226: successful_external_auth_with_valid_cert(Config) ->
  227:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  228:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  229:                                                        [fun s2s_start_stream/2,
  230:                                                         fun s2s_starttls/2,
  231:                                                         fun s2s_external_auth/2]),
  232:     escalus_connection:stop(Client).
  233: 
  234: start_stream_fails_for_wrong_namespace(Config) ->
  235:     start_stream_fails(Config, <<"invalid-namespace">>,
  236:                        [fun s2s_start_stream_with_wrong_namespace/2]).
  237: 
  238: start_stream_fails_for_wrong_version(Config) ->
  239:     %% TLS authentication requires version 1.0
  240:     start_stream_fails(Config, <<"invalid-xml">>,
  241:                        [fun s2s_start_stream_with_wrong_version/2]).
  242: 
  243: start_stream_fails_without_version(Config) ->
  244:     %% TLS authentication requires version 1.0
  245:     start_stream_fails(Config, <<"invalid-xml">>,
  246:                        [fun s2s_start_stream_without_version/2]).
  247: 
  248: start_stream_fails_without_host(Config) ->
  249:     start_stream_fails(Config, <<"improper-addressing">>,
  250:                        [fun s2s_start_stream_without_host/2]).
  251: 
  252: start_stream_fails_for_unknown_host(Config) ->
  253:     start_stream_fails(Config, <<"host-unknown">>,
  254:                        [fun s2s_start_stream_to_wrong_host/2]).
  255: 
  256: starttls_fails_for_unknown_host(Config) ->
  257:     start_stream_fails(Config, <<"host-unknown">>,
  258:                        [fun s2s_start_stream/2,
  259:                         fun s2s_starttls_to_wrong_host/2]).
  260: 
  261: start_stream_fails(Config, ErrorType, ConnectionSteps) ->
  262:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  263:     {ok, Client, _} = escalus_connection:start(ConnectionArgs, ConnectionSteps),
  264:     [Start, Error, End] = escalus:wait_for_stanzas(Client, 3),
  265:     escalus:assert(is_stream_start, Start),
  266:     escalus:assert(is_stream_error, [ErrorType, <<>>], Error),
  267:     escalus:assert(is_stream_end, End).
  268: 
  269: only_messages_from_authenticated_domain_users_are_accepted(Config) ->
  270:     ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config),
  271:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  272:                                                        [fun s2s_start_stream/2,
  273:                                                         fun s2s_starttls/2,
  274:                                                         fun s2s_external_auth/2]),
  275:     escalus:fresh_story(Config, [{alice2, 1}], fun(Alice) ->
  276: 
  277:         UserInWrongDomain = <<"a_user@this_is_not_my.domain.com">>,
  278:         ChatToAliceFromUserInWrongDomain = escalus_stanza:chat(UserInWrongDomain,
  279:                                                                Alice, <<"Miło Cię poznać">>),
  280:         %% Client is a s2s connection established and authenticated for domain "localhost"
  281:         %% Now we try to send a message from other domain than "localhost"
  282:         %% over the established s2s connection
  283:         escalus:send(Client, ChatToAliceFromUserInWrongDomain),
  284: 
  285:         %% Alice@fed1 does not receives message from a_user@this_is_not_my.domain.com
  286:         timer:sleep(timer:seconds(5)),
  287:         escalus_assert:has_no_stanzas(Alice)
  288: 
  289:     end),
  290: 
  291:     escalus_connection:stop(Client).
  292: 
  293: auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert(Config) ->
  294:     ConnectionArgs = connection_args("not_in_cert_domain", <<"not_in_cert_domain">>, Config),
  295:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  296:                                                        [fun s2s_start_stream/2,
  297:                                                         fun s2s_starttls/2]),
  298: 
  299:     try
  300:         escalus_auth:auth_sasl_external(Client, Client#client.props),
  301:         ct:fail("Authenitcated but MUST NOT")
  302:     catch throw:{auth_failed, _, _} ->
  303:               escalus_connection:wait_for_close(Client, timer:seconds(5))
  304:     end.
  305: 
  306: auth_with_valid_cert_fails_for_other_mechanism_than_external(Config) ->
  307:     ConnectionArgs = connection_args("localhost", <<"localhost">>, Config),
  308:     {ok, Client, _Features} = escalus_connection:start(ConnectionArgs,
  309:                                                        [fun s2s_start_stream/2,
  310:                                                         fun s2s_starttls/2
  311:                                                        ]),
  312: 
  313:     Stanza = escalus_stanza:auth(<<"ANONYMOUS">>),
  314:     ok = escalus_connection:send(Client, Stanza),
  315:     #xmlel{name = <<"failure">>} = escalus_connection:get_stanza(Client, wait_for_auth_reply),
  316: 
  317:     escalus_connection:wait_for_close(Client, timer:seconds(5)).
  318: 
  319: connection_args(FromServer, RequestedName, Config) ->
  320:     {KeyFile, CertFile} = get_main_key_and_cert_files(Config),
  321:     [{host, "localhost"},
  322:      {to_server, "fed1"},
  323:      {from_server, FromServer},
  324:      {requested_name, RequestedName},
  325:      {starttls, required},
  326:      {port, ct:get_config({hosts, fed, incoming_s2s_port})},
  327:      {ssl_opts, [{versions, ['tlsv1.2']}, {certfile, CertFile}, {keyfile, KeyFile}]}].
  328: 
  329: s2s_start_stream_with_wrong_namespace(Conn = #client{props = Props}, Features) ->
  330:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"xmlns">> => <<"42">>} end),
  331:     ok = escalus_connection:send(Conn, Start),
  332:     {Conn, Features}.
  333: 
  334: s2s_start_stream_with_wrong_version(Conn = #client{props = Props}, Features) ->
  335:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"version">> => <<"42">>} end),
  336:     ok = escalus_connection:send(Conn, Start),
  337:     {Conn, Features}.
  338: 
  339: s2s_start_stream_without_version(Conn = #client{props = Props}, Features) ->
  340:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"version">>, Attrs) end),
  341:     ok = escalus_connection:send(Conn, Start),
  342:     {Conn, Features}.
  343: 
  344: s2s_start_stream_without_host(Conn = #client{props = Props}, Features) ->
  345:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"to">>, Attrs) end),
  346:     ok = escalus_connection:send(Conn, Start),
  347:     {Conn, Features}.
  348: 
  349: s2s_start_stream_to_wrong_host(Conn = #client{props = Props}, Features) ->
  350:     Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"to">> => <<"42">>} end),
  351:     ok = escalus_connection:send(Conn, Start),
  352:     {Conn, Features}.
  353: 
  354: s2s_start_stream(Conn = #client{props = Props}, []) ->
  355:     StreamStartRep = s2s_start_stream_and_wait_for_response(Conn),
  356: 
  357:     #xmlstreamstart{attrs = Attrs} = StreamStartRep,
  358:     Id = proplists:get_value(<<"id">>, Attrs),
  359: 
  360:     escalus_session:stream_features(Conn#client{props = [{sid, Id} | Props]}, []).
  361: 
  362: s2s_start_stream_and_wait_for_response(Conn = #client{props = Props}) ->
  363:     StreamStart = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs end),
  364:     ok = escalus_connection:send(Conn, StreamStart),
  365:     escalus_connection:get_stanza(Conn, wait_for_stream).
  366: 
  367: s2s_stream_start_stanza(Props, F) ->
  368:     Attrs = (stream_start_attrs())#{<<"to">> => proplists:get_value(to_server, Props),
  369:                                     <<"from">> => proplists:get_value(from_server, Props)},
  370:     #xmlstreamstart{name = <<"stream:stream">>, attrs = maps:to_list(F(Attrs))}.
  371: 
  372: stream_start_attrs() ->
  373:     #{<<"xmlns">> => <<"jabber:server">>,
  374:       <<"xmlns:stream">> => <<"http://etherx.jabber.org/streams">>,
  375:       <<"version">> => <<"1.0">>}.
  376: 
  377: s2s_starttls(Client, Features, StartStreamF) ->
  378:     case proplists:get_value(starttls, Features) of
  379:         false ->
  380:             ct:fail("The server does not offer STARTTLS");
  381:         _ ->
  382:             ok
  383:     end,
  384: 
  385:     escalus_connection:send(Client, escalus_stanza:starttls()),
  386:     escalus_connection:get_stanza(Client, proceed),
  387:     escalus_connection:upgrade_to_tls(Client),
  388:     StartStreamF(Client, []).
  389: 
  390: s2s_starttls(Client, Features) ->
  391:     s2s_starttls(Client, Features, fun s2s_start_stream/2).
  392: 
  393: s2s_starttls_to_wrong_host(Client, Features) ->
  394:     s2s_starttls(Client, Features, fun s2s_start_stream_to_wrong_host/2).
  395: 
  396: s2s_external_auth(Client = #client{props = Props}, Features) ->
  397:     case proplists:get_value(sasl_mechanisms, Features) of
  398:         [<<"EXTERNAL">>] ->
  399:             ok;
  400:         SASL ->
  401:             ct:fail("Server does not provide EXTERNAL auth: ~p", [SASL])
  402:     end,
  403:     escalus_auth:auth_sasl_external(Client, Props),
  404:     s2s_start_stream(Client, []).
  405: 
  406: get_main_key_and_cert_files(Config) ->
  407:     CertFile = get_main_file_path(Config, "cert.pem"),
  408:     KeyFile = get_main_file_path(Config, "key.pem"),
  409:     {KeyFile, CertFile}.
  410: 
  411: get_main_file_path(Config, File) ->
  412:     filename:join([path_helper:repo_dir(Config),
  413:                    "tools", "ssl", "mongooseim", File]).