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