1: %%==============================================================================
    2: %% Copyright 2014 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%==============================================================================
   16: -module(connect_SUITE).
   17: 
   18: -compile([export_all, nowarn_export_all]).
   19: 
   20: -include_lib("common_test/include/ct.hrl").
   21: -include_lib("eunit/include/eunit.hrl").
   22: -include_lib("escalus/include/escalus.hrl").
   23: -include_lib("escalus/include/escalus_xmlns.hrl").
   24: -include_lib("exml/include/exml.hrl").
   25: -include_lib("exml/include/exml_stream.hrl").
   26: -include_lib("stdlib/include/ms_transform.hrl").
   27: -define(assert_equal(E, V), (
   28:     [ct:fail("ASSERT EQUAL~n\tExpected ~p~n\tValue ~p~n", [(E), (V)])
   29:      || (E) =/= (V)])).
   30: -define(SECURE_USER, secure_joe).
   31: -define(CERT_FILE, "priv/ssl/fake_server.pem").
   32: -define(DH_FILE, "priv/ssl/fake_dh_server.pem").
   33: 
   34: -import(distributed_helper, [mim/0,
   35:                              require_rpc_nodes/1,
   36:                              rpc/4]).
   37: 
   38: -import(domain_helper, [domain/0]).
   39: 
   40: %%--------------------------------------------------------------------
   41: %% Suite configuration
   42: %%--------------------------------------------------------------------
   43: 
   44: all() ->
   45:     [
   46:      {group, session_replacement},
   47:      {group, security},
   48:      {group, incorrect_behaviors},
   49:      {group, proxy_protocol},
   50:      %% these groups must be last, as they really... complicate configuration
   51:      {group, fast_tls},
   52:      {group, just_tls}
   53:     ].
   54: 
   55: groups() ->
   56:     G = [ {c2s_noproc, [], [reset_stream_noproc,
   57:                             starttls_noproc,
   58:                             compress_noproc,
   59:                             bad_xml,
   60:                             invalid_host,
   61:                             invalid_stream_namespace,
   62:                             deny_pre_xmpp_1_0_stream]},
   63:           {starttls, [], [should_fail_to_authenticate_without_starttls,
   64:                           should_not_send_other_features_with_starttls_required,
   65:                           auth_bind_pipelined_starttls_skipped_error | protocol_test_cases()]},
   66:           {tls, [parallel], auth_bind_pipelined_cases() ++
   67:                             protocol_test_cases() ++
   68:                             cipher_test_cases()},
   69:           {feature_order, [parallel], [stream_features_test,
   70:                                        tls_authenticate,
   71:                                        tls_compression_fail,
   72:                                        tls_compression_authenticate_fail,
   73:                                        tls_authenticate_compression,
   74:                                        auth_compression_bind_session,
   75:                                        auth_bind_compression_session,
   76:                                        bind_server_generated_resource,
   77:                                        cannot_connect_with_proxy_header]},
   78:           {just_tls, tls_groups()},
   79:           {fast_tls, tls_groups()},
   80:           {session_replacement, [], [
   81:                                      same_resource_replaces_session,
   82:                                      clean_close_of_replaced_session,
   83:                                      replaced_session_cannot_terminate
   84:                                     ]},
   85:           {security, [], [
   86:                           return_proper_stream_error_if_service_is_not_hidden,
   87:                           close_connection_if_service_type_is_hidden
   88:                          ]},
   89:           {incorrect_behaviors, [parallel], [close_connection_if_start_stream_duplicated,
   90:                                              close_connection_if_protocol_violation_after_authentication,
   91:                                              close_connection_if_protocol_violation_after_binding]},
   92:           {proxy_protocol, [parallel], [cannot_connect_without_proxy_header,
   93:                                         connect_with_proxy_header]}
   94:         ],
   95:     ct_helper:repeat_all_until_all_ok(G).
   96: 
   97: tls_groups()->
   98:     [{group, starttls},
   99:      {group, c2s_noproc},
  100:      {group, feature_order},
  101:      {group, tls}].
  102: 
  103: auth_bind_pipelined_cases() ->
  104:     [
  105:      auth_bind_pipelined_session,
  106:      auth_bind_pipelined_auth_failure
  107:     ].
  108: 
  109: protocol_test_cases() ->
  110:     [
  111:      should_fail_with_sslv3,
  112:      should_fail_with_tlsv1,
  113:      should_fail_with_tlsv1_1,
  114:      should_pass_with_tlsv1_2
  115:     ].
  116: 
  117: cipher_test_cases() ->
  118:     [
  119:      %% Server certificate is signed only with RSA for now, don't try to use ECDSA!
  120:      clients_can_connect_with_advertised_ciphers,
  121:      % String cipher
  122:      'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384',
  123:      %% MIM2 accepts ECDHE-RSA-AES256-GCM-SHA384 exclusively with fast_tls on alternative port
  124:      %% MIM3 accepts #{cipher => aes_256_gcm, key_exchange => ecdhe_rsa, mac => aead, prf => sha384}
  125:      %%      exclusively with just_tls on alternative port
  126:      'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384_only'
  127:     ].
  128: 
  129: suite() ->
  130:     require_rpc_nodes([mim, mim2, mim3]) ++ escalus:suite().
  131: 
  132: %%--------------------------------------------------------------------
  133: %% Init & teardown
  134: %%--------------------------------------------------------------------
  135: 
  136: init_per_suite(Config) ->
  137:     Config0 = escalus:init_per_suite([{escalus_user_db, {module, escalus_ejabberd, []}} | Config]),
  138:     C2SPort = ct:get_config({hosts, mim, c2s_port}),
  139:     [C2SListener] = mongoose_helper:get_listeners(mim(), #{port => C2SPort, module => ejabberd_c2s}),
  140:     Config1 = [{c2s_listener, C2SListener} | Config0],
  141:     assert_cert_file_exists(),
  142:     escalus:create_users(Config1, escalus:get_users([?SECURE_USER, alice])).
  143: 
  144: end_per_suite(Config) ->
  145:     escalus_fresh:clean(),
  146:     escalus:delete_users(Config, escalus:get_users([?SECURE_USER, alice])),
  147:     restore_c2s_listener(Config),
  148:     escalus:end_per_suite(Config).
  149: 
  150: init_per_group(c2s_noproc, Config) ->
  151:     configure_c2s_listener(Config, #{tls => [starttls | common_tls_opts(Config)]}),
  152:     Config;
  153: init_per_group(session_replacement, Config) ->
  154:     configure_c2s_listener(Config, #{tls => [starttls | common_tls_opts(Config)]}),
  155:     logger_ct_backend:start(),
  156:     Config;
  157: init_per_group(starttls, Config) ->
  158:     configure_c2s_listener(Config, #{tls => [starttls_required | common_tls_opts(Config)]}),
  159:     Config;
  160: init_per_group(tls, Config) ->
  161:     configure_c2s_listener(Config, #{tls => [tls | common_tls_opts(Config)]}),
  162:     Users = proplists:get_value(escalus_users, Config, []),
  163:     JoeSpec = lists:keydelete(starttls, 1, proplists:get_value(?SECURE_USER, Users)),
  164:     JoeSpec2 = {?SECURE_USER, lists:keystore(ssl, 1, JoeSpec, {ssl, true})},
  165:     NewUsers = lists:keystore(?SECURE_USER, 1, Users, JoeSpec2),
  166:     Config2 = lists:keystore(escalus_users, 1, Config, {escalus_users, NewUsers}),
  167:     [{c2s_port, ct:get_config({hosts, mim, c2s_port})} | Config2];
  168: init_per_group(feature_order, Config) ->
  169:     configure_c2s_listener(Config, #{zlib => 10000,
  170:                                      tls => [starttls_required | common_tls_opts(Config)]}),
  171:     Config;
  172: init_per_group(just_tls, Config)->
  173:     [{tls_module, just_tls} | Config];
  174: init_per_group(fast_tls, Config)->
  175:     [{tls_module, fast_tls} | Config];
  176: init_per_group(proxy_protocol, Config) ->
  177:     configure_c2s_listener(Config, #{proxy_protocol => true}),
  178:     Config;
  179: init_per_group(_, Config) ->
  180:     Config.
  181: 
  182: end_per_group(session_replacement, Config) ->
  183:     logger_ct_backend:stop(),
  184:     Config;
  185: end_per_group(_, Config) ->
  186:     Config.
  187: 
  188: init_per_testcase(close_connection_if_service_type_is_hidden = CN, Config) ->
  189:     Config1 = mongoose_helper:backup_and_set_config_option(Config, hide_service_name, true),
  190:     escalus:init_per_testcase(CN, Config1);
  191: init_per_testcase(replaced_session_cannot_terminate = CN, Config) ->
  192:     S = escalus_users:get_server(Config, alice),
  193:     OptKey = {replaced_wait_timeout, S},
  194:     Config1 = mongoose_helper:backup_and_set_config_option(Config, OptKey, 1),
  195:     escalus:init_per_testcase(CN, Config1);
  196: init_per_testcase(CaseName, Config) ->
  197:     escalus:init_per_testcase(CaseName, Config).
  198: 
  199: end_per_testcase(CaseName, Config) ->
  200:     mongoose_helper:restore_config(Config),
  201:     escalus:end_per_testcase(CaseName, Config).
  202: 
  203: %%--------------------------------------------------------------------
  204: %% Tests
  205: %%--------------------------------------------------------------------
  206: 
  207: bad_xml(Config) ->
  208:     %% given
  209:     Spec = escalus_users:get_userspec(Config, alice),
  210:     %% when
  211:     [Start, Error, End] = connect_with_bad_xml(Spec),
  212:     %% then
  213:     %% See RFC 6120 4.9.1.3 (http://xmpp.org/rfcs/rfc6120.html#streams-error-rules-host).
  214:     %% Stream start from the server is required in this case.
  215:     escalus:assert(is_stream_start, Start),
  216:     escalus:assert(is_stream_error, [<<"xml-not-well-formed">>, <<>>], Error),
  217:     escalus:assert(is_stream_end, End).
  218: 
  219: invalid_host(Config) ->
  220:     %% given
  221:     Spec = escalus_users:get_userspec(Config, alice),
  222:     %% when
  223:     [Start, Error, End] = connect_to_invalid_host(Spec),
  224:     %% then
  225:     %% See RFC 6120 4.9.1.3 (http://xmpp.org/rfcs/rfc6120.html#streams-error-rules-host).
  226:     %% Stream start from the server is required in this case.
  227:     escalus:assert(is_stream_start, Start),
  228:     escalus:assert(is_stream_error, [<<"host-unknown">>, <<>>], Error),
  229:     escalus:assert(is_stream_end, End).
  230: 
  231: invalid_stream_namespace(Config) ->
  232:     %% given
  233:     Spec = escalus_users:get_userspec(Config, alice),
  234:     %% when
  235:     [Start, Error, End] = connect_with_invalid_stream_namespace(Spec),
  236:     %% then
  237:     escalus:assert(is_stream_start, Start),
  238:     escalus:assert(is_stream_error, [<<"invalid-namespace">>, <<>>], Error),
  239:     escalus:assert(is_stream_end, End).
  240: 
  241: deny_pre_xmpp_1_0_stream(Config) ->
  242:     %% given
  243:     Spec = escalus_fresh:freshen_spec(Config, alice),
  244:     Steps = [
  245:              %% when
  246:              {?MODULE, start_stream_pre_xmpp_1_0}
  247:             ],
  248:     {ok, Conn, _} = escalus_connection:start(Spec, Steps),
  249:     StreamError = escalus:wait_for_stanza(Conn),
  250:     escalus:assert(is_stream_error, [<<"unsupported-version">>, <<>>], StreamError),
  251:     escalus_connection:stop(Conn).
  252: 
  253: should_fail_with_sslv3(Config) ->
  254:     should_fail_with(Config, sslv3).
  255: 
  256: should_fail_with_tlsv1(Config) ->
  257:     should_fail_with(Config, tlsv1).
  258: 
  259: should_fail_with_tlsv1_1(Config) ->
  260:     should_fail_with(Config, 'tlsv1.1').
  261: 
  262: should_fail_with(Config, Protocol) ->
  263:     %% Connection process is spawned with a link so besides the crash itself,
  264:     %%   we will receive an exit signal. We don't want to terminate the test due to this.
  265:     %% TODO: Investigate if this behaviour is not a ticking bomb which may affect other test cases.
  266:     process_flag(trap_exit, true),
  267:     %% GIVEN
  268:     UserSpec0 = escalus_users:get_userspec(Config, ?SECURE_USER),
  269:     UserSpec1 = set_secure_connection_protocol(UserSpec0, Protocol),
  270:     %% WHEN
  271:     try escalus_connection:start(UserSpec1) of
  272:     %% THEN
  273:         _ ->
  274:             error({client_connected, Protocol})
  275:     catch
  276:         _C:_R ->
  277:             ok
  278:     end.
  279: 
  280: should_pass_with_tlsv1_2(Config) ->
  281:     UserSpec0 = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  282:     UserSpec1 = set_secure_connection_protocol(UserSpec0, 'tlsv1.2'),
  283: 
  284:     %% WHEN
  285:     Result = escalus_connection:start(UserSpec1),
  286: 
  287:     %% THEN
  288:     ?assertMatch({ok, _, _}, Result).
  289: 
  290: should_fail_to_authenticate_without_starttls(Config) ->
  291:     %% GIVEN
  292:     UserSpec = escalus_users:get_userspec(Config, ?SECURE_USER),
  293:     {Conn, Features} = start_stream_with_compression(UserSpec),
  294: 
  295:     %% WHEN
  296:     try escalus_session:authenticate(Conn, Features) of
  297:     %% THEN
  298:         _ ->
  299:             error(authentication_without_tls_suceeded)
  300:     catch
  301:         throw:{auth_failed, User, AuthReply} ->
  302:             ?assertEqual(atom_to_binary(?SECURE_USER, utf8), User),
  303:             escalus:assert(is_stream_error, [<<"policy-violation">>,
  304:                                              <<"Use of STARTTLS required">>],
  305:                            AuthReply)
  306:     end.
  307: 
  308: should_not_send_other_features_with_starttls_required(Config) ->
  309:     UserSpec = escalus_users:get_userspec(Config, ?SECURE_USER),
  310:     {ok, Conn, _} = escalus_connection:start(UserSpec, [start_stream]),
  311:     Features = case escalus_connection:get_stanza(Conn, wait_for_features) of
  312:         #xmlel{name = <<"stream:features">>, children = Children} -> Children;
  313:         #xmlel{name = <<"features">>, children = Children} -> Children
  314:     end,
  315:     ?assertMatch([#xmlel{name = <<"starttls">>,
  316:                          children = [#xmlel{name = <<"required">>}]}],
  317:                  Features).
  318: 
  319: clients_can_connect_with_advertised_ciphers(Config) ->
  320:     ?assert(length(ciphers_working_with_ssl_clients(Config)) > 0).
  321: 
  322: 'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384'(Config) ->
  323:     ?assert(lists:member("ECDHE-RSA-AES256-GCM-SHA384",
  324:                          ciphers_working_with_ssl_clients(Config))).
  325: 
  326: 'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384_only'(Config) ->
  327:     Port = case ?config(tls_module, Config) of
  328:                just_tls -> ct:get_config({hosts, mim3, c2s_tls_port});
  329:                fast_tls -> ct:get_config({hosts, mim2, c2s_tls_port})
  330:            end,
  331:     Config1 = [{c2s_port, Port} | Config],
  332:     CiphersStr = os:cmd("openssl ciphers 'ECDHE-RSA-AES256-GCM-SHA384'"),
  333:     ct:pal("Available cipher suites for : ~s", [CiphersStr]),
  334:     ct:pal("Openssl version: ~s", [os:cmd("openssl version")]),
  335:     ?assertEqual(["ECDHE-RSA-AES256-GCM-SHA384"],
  336:                  ciphers_working_with_ssl_clients(Config1)).
  337: 
  338: reset_stream_noproc(Config) ->
  339:     UserSpec = escalus_users:get_userspec(Config, alice),
  340:     Steps = [start_stream, stream_features],
  341:     {ok, Conn, _Features} = escalus_connection:start(UserSpec, Steps),
  342: 
  343:     [C2sPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_c2s_sup])),
  344:     [RcvPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_receiver_sup])),
  345:     MonRef = erlang:monitor(process, C2sPid),
  346:     ok = rpc(mim(), sys, suspend, [C2sPid]),
  347:     %% Add auth element into message queue of the c2s process
  348:     %% There is no reply because the process is suspended
  349:     ?assertThrow({timeout, auth_reply}, escalus_session:authenticate(Conn)),
  350:     %% Sim client disconnection
  351:     ok = rpc(mim(), ejabberd_receiver, close, [RcvPid]),
  352:     %% ...c2s process receives close and DOWN messages...
  353:     %% Resume
  354:     ok = rpc(mim(), sys, resume, [C2sPid]),
  355:     receive
  356:         {'DOWN', MonRef, process, C2sPid, normal} ->
  357:             ok;
  358:         {'DOWN', MonRef, process, C2sPid, Reason} ->
  359:             ct:fail("ejabberd_c2s exited with reason ~p", [Reason])
  360:         after 5000 ->
  361:             ct:fail("c2s_monitor_timeout", [])
  362:     end,
  363:     ok.
  364: 
  365: starttls_noproc(Config) ->
  366:     UserSpec = escalus_users:get_userspec(Config, alice),
  367:     Steps = [start_stream, stream_features],
  368:     {ok, Conn, _Features} = escalus_connection:start(UserSpec, Steps),
  369: 
  370:     [C2sPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_c2s_sup])),
  371:     [RcvPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_receiver_sup])),
  372:     MonRef = erlang:monitor(process, C2sPid),
  373:     ok = rpc(mim(), sys, suspend, [C2sPid]),
  374:     %% Add starttls element into message queue of the c2s process
  375:     %% There is no reply because the process is suspended
  376:     ?assertThrow({timeout, proceed}, escalus_session:starttls(Conn)),
  377:     %% Sim client disconnection
  378:     ok = rpc(mim(), ejabberd_receiver, close, [RcvPid]),
  379:     %% ...c2s process receives close and DOWN messages...
  380:     %% Resume
  381:     ok = rpc(mim(), sys, resume, [C2sPid]),
  382:     receive
  383:         {'DOWN', MonRef, process, C2sPid, normal} ->
  384:             ok;
  385:         {'DOWN', MonRef, process, C2sPid, Reason} ->
  386:             ct:fail("ejabberd_c2s exited with reason ~p", [Reason])
  387:         after 5000 ->
  388:             ct:fail("c2s_monitor_timeout", [])
  389:     end,
  390:     ok.
  391: 
  392: compress_noproc(Config) ->
  393:     UserSpec = escalus_users:get_userspec(Config, alice),
  394:     Steps = [start_stream, stream_features],
  395:     {ok, Conn = #client{props = Props}, _Features} = escalus_connection:start(UserSpec, Steps),
  396: 
  397:     [C2sPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_c2s_sup])),
  398:     [RcvPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_receiver_sup])),
  399:     MonRef = erlang:monitor(process, C2sPid),
  400:     ok = rpc(mim(), sys, suspend, [C2sPid]),
  401:     %% Add compress element into message queue of the c2s process
  402:     %% There is no reply because the process is suspended
  403:     ?assertThrow({timeout, compressed},
  404:                  escalus_session:compress(Conn#client{props = [{compression, <<"zlib">>}|Props]})),
  405:     %% Sim client disconnection
  406:     ok = rpc(mim(), ejabberd_receiver, close, [RcvPid]),
  407:     %% ...c2s process receives close and DOWN messages...
  408:     %% Resume
  409:     ok = rpc(mim(), sys, resume, [C2sPid]),
  410:     receive
  411:         {'DOWN', MonRef, process, C2sPid, normal} ->
  412:             ok;
  413:         {'DOWN', MonRef, process, C2sPid, Reason} ->
  414:             ct:fail("ejabberd_c2s exited with reason ~p", [Reason])
  415:         after 5000 ->
  416:             ct:fail("c2s_monitor_timeout", [])
  417:     end,
  418:     ok.
  419: 
  420: %% Tests features advertisement
  421: stream_features_test(Config) ->
  422:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  423:     List = [start_stream, stream_features, {?MODULE, verify_features}],
  424:     escalus_connection:start(UserSpec, List),
  425:     ok.
  426: 
  427: verify_features(Conn, Features) ->
  428:     %% should not advertise compression before tls
  429:     ?assert_equal(false, has_feature(compression, Features)),
  430:     %% start tls. Starttls should be then removed from list and compression should be added
  431:     Conn1 = escalus_session:starttls(Conn),
  432:     {Conn2, Features2} = escalus_session:stream_features(Conn1, []),
  433:     ?assert_equal(false, has_feature(starttls, Features2)),
  434:     ?assert(false =/= has_feature(compression, Features2)),
  435:     %% start compression. Compression should be then removed from list
  436:     {Conn3, _Features3} = escalus_session:authenticate(Conn2, Features2),
  437:     Conn4 = escalus_session:compress(Conn3),
  438:     {Conn5, Features5} = escalus_session:stream_features(Conn4, []),
  439:     ?assert_equal(false, has_feature(compression, Features5)),
  440:     ?assert_equal(false, has_feature(starttls, Features5)),
  441:     {Conn5, Features5}.
  442: 
  443: has_feature(Feature, FeatureList) ->
  444:     {_, Value} = lists:keyfind(Feature, 1, FeatureList),
  445:     Value.
  446: 
  447: %% should fail
  448: tls_compression_authenticate_fail(Config) ->
  449:     %% Given
  450:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  451:     ConnetctionSteps = [start_stream, stream_features, maybe_use_ssl, maybe_use_compression, authenticate],
  452:     %% when and then
  453:     try escalus_connection:start(UserSpec, ConnetctionSteps) of
  454:         _ ->
  455:             error(compression_without_auth_suceeded)
  456:     catch
  457:         error:{assertion_failed, assert, is_compressed, Stanza, _} ->
  458:             case Stanza of
  459:                 #xmlel{name = <<"failure">>} ->
  460:                     ok;
  461:                 _ ->
  462:                     error(unknown_compression_response)
  463:             end
  464:     end.
  465: 
  466: tls_authenticate_compression(Config) ->
  467:     %% Given
  468:     UserSpec = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  469:     ConnectionSteps = [start_stream, stream_features, maybe_use_ssl, authenticate, maybe_use_compression],
  470:     %% when
  471:     {ok, Conn, _} = escalus_connection:start(UserSpec, ConnectionSteps),
  472:     % then
  473:     true = escalus_tcp:is_using_compression(Conn#client.rcv_pid),
  474:     true = escalus_tcp:is_using_ssl(Conn#client.rcv_pid).
  475: 
  476: tls_authenticate(Config) ->
  477:     %% Given
  478:     UserSpec = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  479:     ConnetctionSteps = [start_stream, stream_features, maybe_use_ssl, authenticate],
  480:     %% when
  481:     {ok, Conn, _} = escalus_connection:start(UserSpec, ConnetctionSteps),
  482:     % then
  483:     true = escalus_tcp:is_using_ssl(Conn#client.rcv_pid).
  484: 
  485: %% should fail
  486: tls_compression_fail(Config) ->
  487:     %% Given
  488:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  489:     ConnetctionSteps = [start_stream, stream_features, maybe_use_ssl, maybe_use_compression],
  490:     %% then and when
  491:     try escalus_connection:start(UserSpec, ConnetctionSteps) of
  492:         _ ->
  493:             error(compression_without_auth_suceeded)
  494:     catch
  495:         error:{assertion_failed, assert, is_compressed, Stanza, _} ->
  496:             case Stanza of
  497:                 #xmlel{name = <<"failure">>} ->
  498:                     ok;
  499:                 _ ->
  500:                     error(unknown_compression_response)
  501:             end
  502:     end.
  503: 
  504: auth_compression_bind_session(Config) ->
  505:     %% Given
  506:     UserSpec = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  507:     ConnetctionSteps = [start_stream, stream_features, maybe_use_ssl,
  508:         authenticate, maybe_use_compression, bind, session],
  509:     %% when
  510:     {ok, Conn, _} = escalus_connection:start(UserSpec, ConnetctionSteps),
  511:     % then
  512:     true = escalus_tcp:is_using_compression(Conn#client.rcv_pid).
  513: 
  514: auth_bind_compression_session(Config) ->
  515:     %% Given
  516:     UserSpec = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  517:     ConnetctionSteps = [start_stream, stream_features, maybe_use_ssl,
  518:         authenticate, bind, maybe_use_compression, session],
  519:     %% when
  520:     {ok, Conn, _} = escalus_connection:start(UserSpec, ConnetctionSteps),
  521:     % then
  522:     true = escalus_tcp:is_using_compression(Conn#client.rcv_pid).
  523: 
  524: auth_bind_pipelined_session(Config) ->
  525:     UserSpec = [{ssl, true}, {parser_opts, [{start_tag, <<"stream:stream">>}]}
  526:                 | escalus_fresh:create_fresh_user(Config, alice)],
  527: 
  528:     Username = proplists:get_value(username, UserSpec),
  529:     Conn = pipeline_connect(UserSpec),
  530: 
  531:     %% Stream start
  532:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  533:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  534:     escalus_session:stream_features(Conn, []),
  535: 
  536:     %% Auth response
  537:     escalus_auth:wait_for_success(Username, Conn),
  538:     AuthStreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  539:     ?assertMatch(#xmlstreamstart{}, AuthStreamResponse),
  540:     escalus_session:stream_features(Conn, []),
  541: 
  542:     %% Bind response
  543:     BindResponse = escalus_connection:get_stanza(Conn, bind_response),
  544:     escalus:assert(is_bind_result, BindResponse),
  545: 
  546:     %% Session response
  547:     SessionResponse = escalus_connection:get_stanza(Conn, session_response),
  548:     escalus:assert(is_iq_result, SessionResponse).
  549: 
  550: auth_bind_pipelined_auth_failure(Config) ->
  551:     UserSpec = [{password, <<"badpassword">>}, {ssl, true},
  552:                 {parser_opts, [{start_tag, <<"stream:stream">>}]}
  553:                 | escalus_fresh:freshen_spec(Config, alice)],
  554: 
  555:     Conn = pipeline_connect(UserSpec),
  556: 
  557:     %% Stream start
  558:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  559:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  560:     escalus_session:stream_features(Conn, []),
  561: 
  562:     %% Auth response
  563:     AuthResponse = escalus_connection:get_stanza(Conn, auth_response),
  564:     ?assertMatch(#xmlel{name = <<"failure">>, attrs = [{<<"xmlns">>, ?NS_SASL}]}, AuthResponse).
  565: 
  566: auth_bind_pipelined_starttls_skipped_error(Config) ->
  567:     UserSpec = [{parser_opts, [{start_tag, <<"stream:stream">>}]}
  568:                 | escalus_fresh:freshen_spec(Config, ?SECURE_USER)],
  569: 
  570:     Conn = pipeline_connect(UserSpec),
  571: 
  572:     %% Stream start
  573:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  574:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  575:     escalus_session:stream_features(Conn, []),
  576: 
  577:     %% Auth response
  578:     AuthResponse = escalus_connection:get_stanza(Conn, auth_response),
  579:     escalus:assert(is_stream_error, [<<"policy-violation">>, <<"Use of STARTTLS required">>],
  580:                    AuthResponse).
  581: 
  582: bind_server_generated_resource(Config) ->
  583:     UserSpec = [{resource, <<>>} | escalus_fresh:create_fresh_user(Config, ?SECURE_USER)],
  584:     ConnectionSteps = [start_stream, stream_features, maybe_use_ssl, authenticate, bind],
  585:     {ok, #client{props = NewSpec}, _} = escalus_connection:start(UserSpec, ConnectionSteps),
  586:     {resource, Resource} = lists:keyfind(resource, 1, NewSpec),
  587:     ?assert(is_binary(Resource)),
  588:     ?assert(byte_size(Resource) > 0).
  589: 
  590: same_resource_replaces_session(Config) ->
  591:     UserSpec = [{resource, <<"conflict">>} | escalus_users:get_userspec(Config, alice)],
  592:     {ok, Alice1, _} = escalus_connection:start(UserSpec),
  593: 
  594:     {ok, Alice2, _} = escalus_connection:start(UserSpec),
  595: 
  596:     ConflictError = escalus:wait_for_stanza(Alice1),
  597:     escalus:assert(is_stream_error, [<<"conflict">>, <<>>], ConflictError),
  598: 
  599:     mongoose_helper:wait_until(fun() -> escalus_connection:is_connected(Alice1) end, false),
  600: 
  601:     escalus_connection:stop(Alice2).
  602: 
  603: clean_close_of_replaced_session(Config) ->
  604:     logger_ct_backend:capture(warning),
  605: 
  606:     same_resource_replaces_session(Config),
  607: 
  608:     logger_ct_backend:stop_capture(),
  609:     FilterFun = fun(_, Msg) ->
  610:                         re:run(Msg, "replaced_wait_timeout") /= nomatch
  611:                 end,
  612:     [] = logger_ct_backend:recv(FilterFun).
  613: 
  614: replaced_session_cannot_terminate(Config) ->
  615:     % GIVEN a session that is frozen and cannot terminate
  616:     logger_ct_backend:capture(warning),
  617:     UserSpec = [{resource, <<"conflict">>} | escalus_users:get_userspec(Config, alice)],
  618:     {ok, _Alice1, _} = escalus_connection:start(UserSpec),
  619:     [C2SPid] = children_specs_to_pids(rpc(mim(), supervisor, which_children, [ejabberd_c2s_sup])),
  620:     ok = rpc(mim(), sys, suspend, [C2SPid]),
  621: 
  622:     % WHEN a session gets replaced ...
  623:     {ok, Alice2, _} = escalus_connection:start(UserSpec),
  624: 
  625:     % THEN a timeout warning is logged
  626:     FilterFun = fun(_, Msg) ->
  627:                         re:run(Msg, "replaced_wait_timeout") /= nomatch
  628:                 end,
  629:     mongoose_helper:wait_until(
  630:       fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1),
  631: 
  632:     rpc(mim(), sys, resume, [C2SPid]),
  633:     logger_ct_backend:stop_capture(),
  634: 
  635:     escalus_connection:stop(Alice2).
  636: 
  637: return_proper_stream_error_if_service_is_not_hidden(_Config) ->
  638:     % GIVEN MongooseIM is running default configuration
  639:     % WHEN we send non-XMPP payload
  640:     % THEN the server replies with stream error xml-not-well-formed and closes the connection
  641:     SendMalformedDataStep = fun(Client, Features) ->
  642:                                     escalus_connection:send_raw(Client, <<"malformed">>),
  643:                                     {Client, Features}
  644:                             end,
  645:     {ok, Connection, _} = escalus_connection:start([], [SendMalformedDataStep]),
  646:     escalus_connection:receive_stanza(Connection, #{ assert => is_stream_start }),
  647:     StreamErrorAssertion = {is_stream_error, [<<"xml-not-well-formed">>, <<>>]},
  648:     escalus_connection:receive_stanza(Connection, #{ assert => StreamErrorAssertion }),
  649:     %% Sometimes escalus needs a moment to report the connection as closed
  650:     escalus_connection:wait_for_close(Connection, 5000).
  651: 
  652: close_connection_if_service_type_is_hidden(_Config) ->
  653:     % GIVEN the option to hide service name is enabled
  654:     % WHEN we send non-XMPP payload
  655:     % THEN connection is closed without any response from the server
  656:     FailIfAnyDataReturned = fun(Reply) ->
  657:                                     ct:fail({unexpected_data, Reply})
  658:                             end,
  659:     Connection = escalus_tcp:connect(#{ on_reply => FailIfAnyDataReturned }),
  660:     Ref = monitor(process, Connection),
  661:     escalus_tcp:send(Connection, <<"malformed">>),
  662:     receive
  663:         {'DOWN', Ref, _, _, _} -> ok
  664:     after
  665:         5000 ->
  666:             ct:fail(connection_not_closed)
  667:     end.
  668: 
  669: close_connection_if_start_stream_duplicated(Config) ->
  670:     close_connection_if_protocol_violation(Config, [start_stream, stream_features]).
  671: 
  672: close_connection_if_protocol_violation_after_authentication(Config) ->
  673:     close_connection_if_protocol_violation(Config, [start_stream, stream_features, authenticate]).
  674: 
  675: close_connection_if_protocol_violation_after_binding(Config) ->
  676:     close_connection_if_protocol_violation(Config, [start_stream, stream_features, authenticate, bind]).
  677: 
  678: close_connection_if_protocol_violation(Config, Steps) ->
  679:     AliceSpec = escalus_fresh:create_fresh_user(Config, alice),
  680:     {ok, Alice, _Features} = escalus_connection:start(AliceSpec, Steps),
  681:     escalus:send(Alice, escalus_stanza:stream_start(domain(), ?NS_JABBER_CLIENT)),
  682:     escalus:assert(is_stream_error, [<<"policy-violation">>, <<>>],
  683:                    escalus_connection:get_stanza(Alice, no_stream_error_stanza_received)),
  684:     escalus:assert(is_stream_end,
  685:                    escalus_connection:get_stanza(Alice, no_stream_end_stanza_received)),
  686:     true = escalus_connection:wait_for_close(Alice,timer:seconds(5)).
  687: 
  688: cannot_connect_with_proxy_header(Config) ->
  689:     %% GIVEN proxy protocol is disabled
  690:     UserSpec = escalus_users:get_userspec(Config, alice),
  691: 
  692:     %% WHEN
  693:     ConnectionSteps = [{?MODULE, send_proxy_header}, start_stream],
  694:     ConnResult = escalus_connection:start(UserSpec, ConnectionSteps),
  695: 
  696:     %% THEN
  697:     ?assertMatch({error, {connection_step_failed, _, _}}, ConnResult).
  698: 
  699: cannot_connect_without_proxy_header(Config) ->
  700:     %% GIVEN proxy protocol is enabled
  701:     UserSpec = escalus_users:get_userspec(Config, alice),
  702: 
  703:     %% WHEN
  704:     ConnResult = escalus_connection:start(UserSpec, [start_stream]),
  705: 
  706:     %% THEN
  707:     ?assertMatch({error, {connection_step_failed, _, _}}, ConnResult).
  708: 
  709: connect_with_proxy_header(Config) ->
  710:     %% GIVEN proxy protocol is enabled
  711:     UserSpec = escalus_users:get_userspec(Config, alice),
  712: 
  713:     %% WHEN
  714:     ConnectionSteps = [{?MODULE, send_proxy_header}, start_stream, stream_features,
  715:                        authenticate, bind, session],
  716:     {ok, Conn, _Features} = escalus_connection:start(UserSpec, ConnectionSteps),
  717:     % make sure the session is present
  718:     escalus:send(Conn, escalus_stanza:presence(<<"available">>)),
  719:     escalus:assert(is_presence, escalus:wait_for_stanza(Conn)),
  720: 
  721:     %% THEN
  722:     SessionInfo = mongoose_helper:get_session_info(mim(), Conn),
  723:     #{src_address := IPAddr, src_port := Port} = proxy_info(),
  724:     ?assertMatch({IPAddr, Port}, maps:get(ip, SessionInfo)),
  725:     escalus_connection:stop(Conn).
  726: 
  727: %%--------------------------------------------------------------------
  728: %% Internal functions
  729: %%--------------------------------------------------------------------
  730: 
  731: c2s_port(Config) ->
  732:     case ?config(c2s_port, Config) of
  733:         undefined -> ct:get_config({hosts, mim, c2s_tls_port});
  734:         Value -> Value
  735:     end.
  736: 
  737: ciphers_available_in_os() ->
  738:     CiphersStr = os:cmd("openssl ciphers 'ALL:eNULL'"),
  739:     [string:strip(C, both, $\n) || C <- string:tokens(CiphersStr, ":")].
  740: 
  741: ciphers_working_with_ssl_clients(Config) ->
  742:     Port = c2s_port(Config),
  743:     lists:filter(fun(Cipher) ->
  744:                          openssl_client_can_use_cipher(Cipher, Port)
  745:                  end, ciphers_available_in_os()).
  746: 
  747: openssl_client_can_use_cipher(Cipher, Port) ->
  748:     PortStr = integer_to_list(Port),
  749:     Cmd = "echo '' | openssl s_client -connect localhost:" ++ PortStr ++
  750:           " -cipher \"" ++ Cipher ++ "\" -tls1_2 2>&1",
  751:     Output = os:cmd(Cmd),
  752:     0 == string:str(Output, ":error:") andalso 0 == string:str(Output, "errno=0").
  753: 
  754: restore_c2s_listener(Config) ->
  755:     C2SListener = ?config(c2s_listener, Config),
  756:     mongoose_helper:restart_listener(mim(), C2SListener).
  757: 
  758: assert_cert_file_exists() ->
  759:     ejabberd_node_utils:file_exists(?CERT_FILE) orelse
  760:         ct:fail("cert file ~s not exists", [?CERT_FILE]).
  761: 
  762: configure_c2s_listener(Config, ExtraC2SOpts) ->
  763:     C2SListener = ?config(c2s_listener, Config),
  764:     NewC2SListener = maps:merge(C2SListener, ExtraC2SOpts),
  765:     mongoose_helper:restart_listener(mim(), NewC2SListener).
  766: 
  767: common_tls_opts(Config) ->
  768:     tls_module_opts(Config) ++ [{certfile, ?CERT_FILE}, {dhfile, ?DH_FILE}].
  769: 
  770: tls_module_opts(Config) ->
  771:     case ?config(tls_module, Config) of
  772:         undefined -> [];
  773:         Module -> [{tls_module, Module}]
  774:     end.
  775: 
  776: set_secure_connection_protocol(UserSpec, Version) ->
  777:     [{ssl_opts, [{versions, [Version]}]} | UserSpec].
  778: 
  779: start_stream_with_compression(UserSpec) ->
  780:     ConnectionSteps = [start_stream, stream_features, maybe_use_compression],
  781:     {ok, Conn, Features} = escalus_connection:start(UserSpec, ConnectionSteps),
  782:     {Conn, Features}.
  783: 
  784: connect_to_invalid_host(Spec) ->
  785:     {ok, Conn, _} = escalus_connection:start(Spec, [{?MODULE, connect_to_invalid_host}]),
  786:     escalus:wait_for_stanzas(Conn, 3).
  787: 
  788: connect_to_invalid_host(Conn, UnusedFeatures) ->
  789:     escalus:send(Conn, escalus_stanza:stream_start(<<"hopefullynonexistentdomain">>,
  790:                                                    ?NS_JABBER_CLIENT)),
  791:     {Conn, UnusedFeatures}.
  792: 
  793: connect_with_bad_xml(Spec) ->
  794:     {ok, Conn, _} = escalus_connection:start(Spec, [{?MODULE, connect_with_bad_xml}]),
  795:     escalus:wait_for_stanzas(Conn, 3).
  796: 
  797: connect_with_bad_xml(Conn, UnusedFeatures) ->
  798:     escalus_connection:send(Conn, #xmlcdata{content = "asdf\n"}),
  799:     {Conn, UnusedFeatures}.
  800: 
  801: connect_with_invalid_stream_namespace(Spec) ->
  802:     F = fun (Conn, UnusedFeatures) ->
  803:                 Start = stream_start_invalid_stream_ns(escalus_users:get_server([], Spec)),
  804:                 escalus:send(Conn, Start),
  805:                 {Conn, UnusedFeatures}
  806:         end,
  807:     {ok, Conn, _} = escalus_connection:start(Spec, [F]),
  808:     escalus:wait_for_stanzas(Conn, 3).
  809: 
  810: start_stream_pre_xmpp_1_0(Conn = #client{props = Props}, UnusedFeatures) ->
  811:     escalus:send(Conn, stream_start_pre_xmpp_1_0(escalus_users:get_server([], Props))),
  812:     #xmlstreamstart{attrs = StreamAttrs} = StreamStart = escalus:wait_for_stanza(Conn),
  813:     escalus:assert(is_stream_start, StreamStart),
  814:     {<<"id">>, StreamID} = lists:keyfind(<<"id">>, 1, StreamAttrs),
  815:     {Conn#client{props = [{stream_id, StreamID} | Props]}, UnusedFeatures}.
  816: 
  817: stream_start_pre_xmpp_1_0(To) ->
  818:         stream_start(lists:keystore(version, 1, default_context(To), {version, <<>>})).
  819: 
  820: stream_start(Context) ->
  821:     %% Be careful! The closing slash here is a hack to enable implementation of from_template/2
  822:     %% to parse the snippet properly. In standard XMPP <stream:stream> is just opening of an XML
  823:     %% element, NOT A SELF CLOSING element.
  824:     T = <<"<stream:stream {{version}} xml:lang='en' xmlns='jabber:client' "
  825:           "               to='{{to}}' "
  826:           "               xmlns:stream='{{stream_ns}}' />">>,
  827:     %% So we rewrap the parsed contents from #xmlel{} to #xmlstreamstart{} here.
  828:     #xmlel{name = Name, attrs = Attrs, children = []} = escalus_stanza:from_template(T, Context),
  829:     #xmlstreamstart{name = Name, attrs = Attrs}.
  830: 
  831: stream_start_invalid_stream_ns(To) ->
  832:     stream_start(lists:keystore(stream_ns, 1, default_context(To),
  833:                                 {stream_ns, <<"obviously-invalid-namespace">>})).
  834: 
  835: default_context(To) ->
  836:     [{version, <<"version='1.0'">>},
  837:      {to, To},
  838:      {stream_ns, ?NS_XMPP}].
  839: 
  840: children_specs_to_pids(Children) ->
  841:     [Pid || {_, Pid, _, _} <- Children].
  842: 
  843: pipeline_connect(UserSpec) ->
  844:     Server = proplists:get_value(server, UserSpec),
  845:     Username = proplists:get_value(username, UserSpec),
  846:     Password = proplists:get_value(password, UserSpec),
  847:     AuthPayload = <<0:8, Username/binary, 0:8, Password/binary>>,
  848: 
  849:     Conn = escalus_connection:connect(UserSpec),
  850: 
  851:     Stream = escalus_stanza:stream_start(Server, <<"jabber:client">>),
  852:     Auth = escalus_stanza:auth(<<"PLAIN">>, [#xmlcdata{content = base64:encode(AuthPayload)}]),
  853:     AuthStream = escalus_stanza:stream_start(Server, <<"jabber:client">>),
  854:     Bind = escalus_stanza:bind(<<?MODULE_STRING "_resource">>),
  855:     Session = escalus_stanza:session(),
  856: 
  857:     escalus_connection:send(Conn, [Stream, Auth, AuthStream, Bind, Session]),
  858:     Conn.
  859: 
  860: send_proxy_header(Conn, UnusedFeatures) ->
  861:     Header = ranch_proxy_header:header(proxy_info()),
  862:     escalus_connection:send_raw(Conn, iolist_to_binary(Header)),
  863:     {Conn, UnusedFeatures}.
  864: 
  865: proxy_info() ->
  866:     #{version => 2,
  867:       command => proxy,
  868:       transport_family => ipv4,
  869:       transport_protocol => stream,
  870:       src_address => {1, 2, 3, 4},
  871:       src_port => 444,
  872:       dest_address => {192, 168, 0, 1},
  873:       dest_port => 443
  874:      }.