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">>.