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(CACERT_FILE, "priv/ssl/cacert.pem").
   32: -define(CERT_FILE, "priv/ssl/fake_server.pem").
   33: -define(DH_FILE, "priv/ssl/fake_dh_server.pem").
   34: 
   35: -import(distributed_helper, [mim/0,
   36:                              mim2/0,
   37:                              mim3/0,
   38:                              require_rpc_nodes/1,
   39:                              rpc/4]).
   40: -import(domain_helper, [domain/0]).
   41: -import(config_parser_helper, [default_c2s_tls/1]).
   42: 
   43: %%--------------------------------------------------------------------
   44: %% Suite configuration
   45: %%--------------------------------------------------------------------
   46: 
   47: all() ->
   48:     [
   49:         {group, session_replacement},
   50:         {group, security},
   51:         {group, incorrect_behaviors},
   52:         {group, proxy_protocol},
   53:         %% these groups must be last, as they really... complicate configuration
   54:         {group, fast_tls},
   55:         {group, just_tls}
   56:     ].
   57: 
   58: groups() ->
   59:     [
   60:         {starttls_disabled, [parallel], [correct_features_are_advertised_for_disabled_starttls,
   61:                                          starttls_should_fail_when_disabled]},
   62:         {starttls_optional, [parallel], [bad_xml,
   63:                                          invalid_host,
   64:                                          invalid_stream_namespace,
   65:                                          deny_pre_xmpp_1_0_stream,
   66:                                          correct_features_are_advertised_for_optional_starttls]},
   67:         {starttls_required, [], [{group, starttls_required_parallel}, metrics_test]},
   68:         {starttls_required_parallel, [parallel], [correct_features_are_advertised_for_required_starttls,
   69:                                                   tls_authenticate,
   70:                                                   bind_server_generated_resource,
   71:                                                   cannot_connect_with_proxy_header,
   72:                                                   should_fail_to_authenticate_without_starttls,
   73:                                                   auth_bind_pipelined_starttls_skipped_error
   74:                                                  | protocol_test_cases()]},
   75:         {tls, [parallel], auth_bind_pipelined_cases() ++
   76:                           protocol_test_cases() ++
   77:                           cipher_test_cases()},
   78:         {just_tls, tls_groups()},
   79:         {fast_tls, tls_groups()},
   80:         {session_replacement, [], [same_resource_replaces_session,
   81:                                    clean_close_of_replaced_session,
   82:                                    replaced_session_cannot_terminate,
   83:                                    replaced_session_cannot_terminate_different_nodes]},
   84:         {security, [], [return_proper_stream_error_if_service_is_not_hidden,
   85:                         close_connection_if_service_type_is_hidden]},
   86:         {incorrect_behaviors, [parallel], [close_connection_if_start_stream_duplicated,
   87:                                            close_connection_if_protocol_violation_after_authentication,
   88:                                            close_connection_if_protocol_violation_after_binding]},
   89:         {proxy_protocol, [parallel], [cannot_connect_without_proxy_header,
   90:                                       connect_with_proxy_header]}
   91:     ].
   92: 
   93: tls_groups()->
   94:     [
   95:         {group, starttls_disabled},
   96:         {group, starttls_optional},
   97:         {group, starttls_required},
   98:         {group, tls}
   99:     ].
  100: 
  101: auth_bind_pipelined_cases() ->
  102:     [
  103:         auth_bind_pipelined_session,
  104:         auth_bind_pipelined_auth_failure
  105:     ].
  106: 
  107: protocol_test_cases() ->
  108:     [
  109:         should_fail_with_sslv3,
  110:         should_fail_with_tlsv1,
  111:         should_fail_with_tlsv1_1,
  112:         should_pass_with_tlsv1_2
  113:     ].
  114: 
  115: cipher_test_cases() ->
  116:     [
  117:         %% Server certificate is signed only with RSA for now, don't try to use ECDSA!
  118:         clients_can_connect_with_advertised_ciphers,
  119:         % String cipher
  120:         'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384',
  121:         %% MIM2 accepts ECDHE-RSA-AES256-GCM-SHA384 exclusively with fast_tls on alternative port
  122:         %% MIM3 accepts #{cipher => aes_256_gcm, key_exchange => ecdhe_rsa, mac => aead, prf => sha384}
  123:         %%      exclusively with just_tls on alternative port
  124:         'clients_can_connect_with_ECDHE-RSA-AES256-GCM-SHA384_only'
  125:     ].
  126: 
  127: suite() ->
  128:     require_rpc_nodes([mim, mim2, mim3]) ++ escalus:suite().
  129: 
  130: %%--------------------------------------------------------------------
  131: %% Init & teardown
  132: %%--------------------------------------------------------------------
  133: 
  134: init_per_suite(Config) ->
  135:     Config0 = escalus:init_per_suite([{escalus_user_db, {module, escalus_ejabberd, []}} | Config]),
  136:     C2SPort = ct:get_config({hosts, mim, c2s_port}),
  137:     [C2SListener] = mongoose_helper:get_listeners(mim(), #{port => C2SPort, module => mongoose_c2s_listener}),
  138:     Config1 = [{c2s_listener, C2SListener} | Config0],
  139:     assert_cert_file_exists(),
  140:     escalus:create_users(Config1, escalus:get_users([?SECURE_USER, alice])).
  141: 
  142: end_per_suite(Config) ->
  143:     escalus_fresh:clean(),
  144:     escalus:delete_users(Config, escalus:get_users([?SECURE_USER, alice])),
  145:     restore_c2s_listener(Config),
  146:     escalus:end_per_suite(Config).
  147: 
  148: init_per_group(starttls_optional, Config) ->
  149:     configure_c2s_listener(Config, #{tls => tls_opts(starttls, Config)}),
  150:     Config;
  151: init_per_group(session_replacement, Config) ->
  152:     configure_c2s_listener(Config, #{tls => tls_opts(starttls, Config)}),
  153:     logger_ct_backend:start(),
  154:     Config;
  155: init_per_group(starttls_required, Config) ->
  156:     configure_c2s_listener(Config, #{tls => tls_opts(starttls_required, Config)}),
  157:     Config;
  158: init_per_group(starttls_disabled, Config) ->
  159:     configure_c2s_listener(Config, #{}, [tls]),
  160:     Config;
  161: init_per_group(tls, Config) ->
  162:     configure_c2s_listener(Config, #{tls => tls_opts(tls, Config)}),
  163:     Users = proplists:get_value(escalus_users, Config, []),
  164:     JoeSpec = lists:keydelete(starttls, 1, proplists:get_value(?SECURE_USER, Users)),
  165:     JoeSpec2 = {?SECURE_USER, lists:keystore(ssl, 1, JoeSpec, {ssl, true})},
  166:     NewUsers = lists:keystore(?SECURE_USER, 1, Users, JoeSpec2),
  167:     Config2 = lists:keystore(escalus_users, 1, Config, {escalus_users, NewUsers}),
  168:     [{c2s_port, ct:get_config({hosts, mim, c2s_port})} | Config2];
  169: init_per_group(just_tls, Config)->
  170:     [{tls_module, just_tls} | Config];
  171: init_per_group(fast_tls, Config)->
  172:     [{tls_module, fast_tls} | Config];
  173: init_per_group(proxy_protocol, Config) ->
  174:     configure_c2s_listener(Config, #{proxy_protocol => true}),
  175:     Config;
  176: init_per_group(_, Config) ->
  177:     Config.
  178: 
  179: end_per_group(session_replacement, Config) ->
  180:     logger_ct_backend:stop(),
  181:     Config;
  182: end_per_group(_, Config) ->
  183:     Config.
  184: 
  185: init_per_testcase(close_connection_if_service_type_is_hidden = CN, Config) ->
  186:     Config1 = mongoose_helper:backup_and_set_config_option(Config, hide_service_name, true),
  187:     escalus:init_per_testcase(CN, Config1);
  188: init_per_testcase(replaced_session_cannot_terminate = CN, Config) ->
  189:     S = escalus_users:get_server(Config, alice),
  190:     OptKey = {replaced_wait_timeout, S},
  191:     Config1 = mongoose_helper:backup_and_set_config_option(Config, OptKey, 1),
  192:     escalus:init_per_testcase(CN, Config1);
  193: init_per_testcase(replaced_session_cannot_terminate_different_nodes = CN, Config) ->
  194:     S = escalus_users:get_server(Config, alice),
  195:     OptKey = {replaced_wait_timeout, S},
  196:     Config1 = mongoose_helper:backup_and_set_config_option(Config, OptKey, 1),
  197:     Config2 = distributed_helper:add_node_to_cluster(mim2(), Config1),
  198:     logger_ct_backend:start(mim2()),
  199:     escalus:init_per_testcase(CN, Config2);
  200: init_per_testcase(CaseName, Config) ->
  201:     escalus:init_per_testcase(CaseName, Config).
  202: 
  203: end_per_testcase(replaced_session_cannot_terminate_different_nodes = CaseName, Config) ->
  204:     logger_ct_backend:stop(mim2()),
  205:     distributed_helper:remove_node_from_cluster(mim2(), Config),
  206:     mongoose_helper:restore_config(Config),
  207:     escalus:end_per_testcase(CaseName, Config);
  208: end_per_testcase(CaseName, Config) ->
  209:     mongoose_helper:restore_config(Config),
  210:     escalus:end_per_testcase(CaseName, Config).
  211: 
  212: %%--------------------------------------------------------------------
  213: %% Tests
  214: %%--------------------------------------------------------------------
  215: 
  216: bad_xml(Config) ->
  217:     %% given
  218:     Spec = escalus_users:get_userspec(Config, alice),
  219:     %% when
  220:     [Start, Error, End] = connect_with_bad_xml(Spec),
  221:     %% then
  222:     %% See RFC 6120 4.9.1.3 (http://xmpp.org/rfcs/rfc6120.html#streams-error-rules-host).
  223:     %% Stream start from the server is required in this case.
  224:     escalus:assert(is_stream_start, Start),
  225:     escalus:assert(is_stream_error, [<<"xml-not-well-formed">>, <<>>], Error),
  226:     escalus:assert(is_stream_end, End).
  227: 
  228: invalid_host(Config) ->
  229:     %% given
  230:     Spec = escalus_users:get_userspec(Config, alice),
  231:     %% when
  232:     [Start, Error, End] = connect_to_invalid_host(Spec),
  233:     %% then
  234:     %% See RFC 6120 4.9.1.3 (http://xmpp.org/rfcs/rfc6120.html#streams-error-rules-host).
  235:     %% Stream start from the server is required in this case.
  236:     escalus:assert(is_stream_start, Start),
  237:     escalus:assert(is_stream_error, [<<"host-unknown">>, <<>>], Error),
  238:     escalus:assert(is_stream_end, End).
  239: 
  240: invalid_stream_namespace(Config) ->
  241:     %% given
  242:     Spec = escalus_users:get_userspec(Config, alice),
  243:     %% when
  244:     [Start, Error, End] = connect_with_invalid_stream_namespace(Spec),
  245:     %% then
  246:     escalus:assert(is_stream_start, Start),
  247:     escalus:assert(is_stream_error, [<<"invalid-namespace">>, <<>>], Error),
  248:     escalus:assert(is_stream_end, End).
  249: 
  250: deny_pre_xmpp_1_0_stream(Config) ->
  251:     %% given
  252:     Spec = escalus_fresh:freshen_spec(Config, alice),
  253:     Steps = [
  254:              %% when
  255:              {?MODULE, start_stream_pre_xmpp_1_0}
  256:             ],
  257:     {ok, Conn, _} = escalus_connection:start(Spec, Steps),
  258:     StreamError = escalus:wait_for_stanza(Conn),
  259:     escalus:assert(is_stream_error, [<<"unsupported-version">>, <<>>], StreamError),
  260:     escalus_connection:stop(Conn).
  261: 
  262: should_fail_with_sslv3(Config) ->
  263:     should_fail_with(Config, sslv3).
  264: 
  265: should_fail_with_tlsv1(Config) ->
  266:     should_fail_with(Config, tlsv1).
  267: 
  268: should_fail_with_tlsv1_1(Config) ->
  269:     should_fail_with(Config, 'tlsv1.1').
  270: 
  271: should_fail_with(Config, Protocol) ->
  272:     %% Connection process is spawned with a link so besides the crash itself,
  273:     %%   we will receive an exit signal. We don't want to terminate the test due to this.
  274:     %% TODO: Investigate if this behaviour is not a ticking bomb which may affect other test cases.
  275:     process_flag(trap_exit, true),
  276:     %% GIVEN
  277:     UserSpec0 = escalus_users:get_userspec(Config, ?SECURE_USER),
  278:     UserSpec1 = set_secure_connection_protocol(UserSpec0, Protocol),
  279:     %% WHEN
  280:     try escalus_connection:start(UserSpec1) of
  281:     %% THEN
  282:         _ ->
  283:             error({client_connected, Protocol})
  284:     catch
  285:         _C:_R ->
  286:             ok
  287:     end.
  288: 
  289: should_pass_with_tlsv1_2(Config) ->
  290:     UserSpec0 = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  291:     UserSpec1 = set_secure_connection_protocol(UserSpec0, 'tlsv1.2'),
  292: 
  293:     %% WHEN
  294:     Result = escalus_connection:start(UserSpec1),
  295: 
  296:     %% THEN
  297:     ?assertMatch({ok, _, _}, Result).
  298: 
  299: should_fail_to_authenticate_without_starttls(Config) ->
  300:     %% GIVEN
  301:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  302:     ConnectionSteps = [start_stream, stream_features],
  303:     {ok, Conn, Features} = escalus_connection:start(UserSpec, ConnectionSteps),
  304:     UserName = escalus_utils:get_username(Conn),
  305: 
  306:     %% WHEN
  307:     try escalus_session:authenticate(Conn, Features) of
  308:     %% THEN
  309:         _ ->
  310:             error(authentication_without_tls_suceeded)
  311:     catch
  312:         throw:{auth_failed, User, AuthReply} ->
  313:             ?assertEqual(UserName, User),
  314:             escalus:assert(is_stream_error, [<<"policy-violation">>,
  315:                                              <<"Use of STARTTLS required">>],
  316:                            AuthReply)
  317:     end.
  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: correct_features_are_advertised_for_disabled_starttls(Config) ->
  339:     UserSpec = escalus_fresh:freshen_spec(Config, alice),
  340:     Steps = [start_stream,
  341:              stream_features,
  342:              {?MODULE, verify_features_without_starttls},
  343:              authenticate],
  344:     escalus_connection:start(UserSpec, Steps).
  345: 
  346: correct_features_are_advertised_for_optional_starttls(Config) ->
  347:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  348:     Steps = [start_stream,
  349:              stream_features,
  350:              {?MODULE, verify_features_with_optional_starttls},
  351:              maybe_use_ssl,
  352:              {?MODULE, verify_features_without_starttls},
  353:              authenticate],
  354:     escalus_connection:start(UserSpec ++ [{ssl_opts, [{verify, verify_none}]}], Steps).
  355: 
  356: correct_features_are_advertised_for_required_starttls(Config) ->
  357:     UserSpec = escalus_fresh:freshen_spec(Config, ?SECURE_USER),
  358:     Steps = [start_stream,
  359:              stream_features,
  360:              {?MODULE, verify_features_with_required_starttls},
  361:              maybe_use_ssl,
  362:              {?MODULE, verify_features_without_starttls},
  363:              authenticate],
  364:     escalus_connection:start(UserSpec ++ [{ssl_opts, [{verify, verify_none}]}], Steps).
  365: 
  366: verify_features_without_starttls(Conn, Features) ->
  367:     ?assertEqual({starttls, false}, get_feature(starttls, Features)),
  368:     ?assertMatch({sasl_mechanisms, [_|_]}, get_feature(sasl_mechanisms, Features)),
  369:     {Conn, Features}.
  370: 
  371: verify_features_with_optional_starttls(Conn, Features) ->
  372:     ?assertEqual({starttls, true}, get_feature(starttls, Features)),
  373:     ?assertMatch({sasl_mechanisms, [_|_]}, get_feature(sasl_mechanisms, Features)),
  374:     {Conn, Features}.
  375: 
  376: verify_features_with_required_starttls(Conn, Features) ->
  377:     AdvertisedFeatures = lists:filter(fun is_present/1, Features),
  378:     ?assertEqual([{starttls, true}], AdvertisedFeatures),
  379:     {Conn, Features}.
  380: 
  381: is_present({_, Value}) ->
  382:     Value =/= false andalso Value =/= [] andalso Value =/= undefined.
  383: 
  384: get_feature(Feature, FeatureList) ->
  385:     lists:keyfind(Feature, 1, FeatureList).
  386: 
  387: starttls_should_fail_when_disabled(Config) ->
  388:     UserSpec = escalus_fresh:freshen_spec(Config, alice),
  389:     List = [start_stream, stream_features],
  390:     {ok, Conn, _Features} =
  391:         escalus_connection:start(UserSpec ++ [{ssl_opts, [{verify, verify_none}]}], List),
  392: 
  393:     %% Client tries to start tls anyway, and fails
  394:     escalus_connection:send(Conn, escalus_stanza:starttls()),
  395:     Result = escalus_connection:get_stanza(Conn, failure),
  396:     %% As defined in https://datatracker.ietf.org/doc/html/rfc6120#section-5.4.2.2, cause 2
  397:     ?assertEqual(<<"failure">>, Result#xmlel.name),
  398:     escalus:assert(has_ns, [?NS_TLS], Result),
  399:     escalus_connection:wait_for_close(Conn, timer:seconds(5)).
  400: 
  401: metrics_test(Config) ->
  402:     MongooseMetrics = [{[global, data, xmpp, received, xml_stanza_size], changed},
  403:                        {[global, data, xmpp, sent, xml_stanza_size], changed},
  404:                        {[global, data, xmpp, received, c2s, tls], changed},
  405:                        {[global, data, xmpp, sent, c2s, tls], changed},
  406:                        %% TCP traffic before starttls
  407:                        {[global, data, xmpp, received, c2s, tcp], changed},
  408:                        {[global, data, xmpp, sent, c2s, tcp], changed}],
  409:     PreStoryData = escalus_mongooseim:pre_story([{mongoose_metrics, MongooseMetrics}]),
  410:     tls_authenticate(Config),
  411:     escalus_mongooseim:post_story(PreStoryData).
  412: 
  413: tls_authenticate(Config) ->
  414:     %% Given
  415:     UserSpec = escalus_fresh:create_fresh_user(Config, ?SECURE_USER),
  416:     ConnectionSteps = [start_stream, stream_features, maybe_use_ssl, authenticate],
  417:     %% when
  418:     {ok, Conn, _} = escalus_connection:start(UserSpec ++ [{ssl_opts, [{verify, verify_none}]}], ConnectionSteps),
  419:     % then
  420:     true = escalus_tcp:is_using_ssl(Conn#client.rcv_pid).
  421: 
  422: auth_bind_pipelined_session(Config) ->
  423:     UserSpec = [{ssl, true}, {parser_opts, [{start_tag, <<"stream:stream">>}]},
  424:                 {ssl_opts, [{verify, verify_none}]}
  425:                 | escalus_fresh:create_fresh_user(Config, alice)],
  426: 
  427:     Username = proplists:get_value(username, UserSpec),
  428:     Conn = pipeline_connect(UserSpec),
  429: 
  430:     %% Stream start
  431:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  432:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  433:     escalus_session:stream_features(Conn, []),
  434: 
  435:     %% Auth response
  436:     escalus_auth:wait_for_success(Username, Conn),
  437:     AuthStreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  438:     ?assertMatch(#xmlstreamstart{}, AuthStreamResponse),
  439:     escalus_session:stream_features(Conn, []),
  440: 
  441:     %% Bind response
  442:     BindResponse = escalus_connection:get_stanza(Conn, bind_response),
  443:     escalus:assert(is_bind_result, BindResponse),
  444: 
  445:     %% Session response
  446:     SessionResponse = escalus_connection:get_stanza(Conn, session_response),
  447:     escalus:assert(is_iq_result, SessionResponse).
  448: 
  449: auth_bind_pipelined_auth_failure(Config) ->
  450:     UserSpec = [{password, <<"badpassword">>}, {ssl, true},
  451:                 {ssl_opts, [{verify, verify_none}]},
  452:                 {parser_opts, [{start_tag, <<"stream:stream">>}]}
  453:                 | escalus_fresh:freshen_spec(Config, alice)],
  454: 
  455:     Conn = pipeline_connect(UserSpec),
  456: 
  457:     %% Stream start
  458:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  459:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  460:     escalus_session:stream_features(Conn, []),
  461: 
  462:     %% Auth response
  463:     AuthResponse = escalus_connection:get_stanza(Conn, auth_response),
  464:     ?assertMatch(#xmlel{name = <<"failure">>, attrs = [{<<"xmlns">>, ?NS_SASL}]}, AuthResponse).
  465: 
  466: auth_bind_pipelined_starttls_skipped_error(Config) ->
  467:     UserSpec = [{parser_opts, [{start_tag, <<"stream:stream">>}]}
  468:                 | escalus_fresh:freshen_spec(Config, ?SECURE_USER)],
  469: 
  470:     Conn = pipeline_connect(UserSpec),
  471: 
  472:     %% Stream start
  473:     StreamResponse = escalus_connection:get_stanza(Conn, stream_response),
  474:     ?assertMatch(#xmlstreamstart{}, StreamResponse),
  475:     escalus_session:stream_features(Conn, []),
  476: 
  477:     %% Auth response
  478:     AuthResponse = escalus_connection:get_stanza(Conn, auth_response),
  479:     escalus:assert(is_stream_error, [<<"policy-violation">>, <<"Use of STARTTLS required">>],
  480:                    AuthResponse).
  481: 
  482: bind_server_generated_resource(Config) ->
  483:     UserSpec = [{resource, <<>>}, {ssl_opts, [{verify, verify_none}]}
  484:                 | escalus_fresh:create_fresh_user(Config, ?SECURE_USER)],
  485:     ConnectionSteps = [start_stream, stream_features, maybe_use_ssl, authenticate, bind],
  486:     {ok, #client{props = NewSpec}, _} = escalus_connection:start(UserSpec, ConnectionSteps),
  487:     {resource, Resource} = lists:keyfind(resource, 1, NewSpec),
  488:     ?assert(is_binary(Resource)),
  489:     ?assert(byte_size(Resource) > 0).
  490: 
  491: same_resource_replaces_session(Config) ->
  492:     UserSpec = [{resource, <<"conflict">>} | escalus_users:get_userspec(Config, alice)],
  493:     {ok, Alice1, _} = escalus_connection:start(UserSpec),
  494: 
  495:     {ok, Alice2, _} = escalus_connection:start(UserSpec),
  496: 
  497:     ConflictError = escalus:wait_for_stanza(Alice1),
  498:     escalus:assert(is_stream_error, [<<"conflict">>, <<>>], ConflictError),
  499: 
  500:     mongoose_helper:wait_until(fun() -> escalus_connection:is_connected(Alice1) end, false),
  501: 
  502:     escalus_connection:stop(Alice2).
  503: 
  504: clean_close_of_replaced_session(Config) ->
  505:     logger_ct_backend:capture(warning),
  506: 
  507:     same_resource_replaces_session(Config),
  508: 
  509:     logger_ct_backend:stop_capture(),
  510:     FilterFun = fun(_, Msg) ->
  511:                         re:run(Msg, "replaced_wait_timeout") /= nomatch
  512:                 end,
  513:     [] = logger_ct_backend:recv(FilterFun).
  514: 
  515: replaced_session_cannot_terminate(Config) ->
  516:     % GIVEN a session that is frozen and cannot terminate
  517:     logger_ct_backend:capture(warning),
  518:     UserSpec = [{resource, <<"conflict">>} | escalus_users:get_userspec(Config, alice)],
  519:     {ok, _Alice1, _} = escalus_connection:start(UserSpec),
  520:     [C2SPid] = ranch_procs(),
  521:     ok = rpc(mim(), sys, suspend, [C2SPid]),
  522: 
  523:     % WHEN a session gets replaced ...
  524:     {ok, Alice2, _} = escalus_connection:start(UserSpec),
  525: 
  526:     % THEN a timeout warning is logged
  527:     FilterFun = fun(_, Msg) ->
  528:                         re:run(Msg, "replaced_wait_timeout") /= nomatch
  529:                 end,
  530:     mongoose_helper:wait_until(
  531:       fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1),
  532: 
  533:     rpc(mim(), sys, resume, [C2SPid]),
  534:     logger_ct_backend:stop_capture(),
  535: 
  536:     escalus_connection:stop(Alice2).
  537: 
  538: replaced_session_cannot_terminate_different_nodes(Config) ->
  539:     % GIVEN a session that is frozen and cannot terminate
  540:     logger_ct_backend:capture(warning, mim2()),
  541:     UserSpec = [{resource, <<"conflict">>} | escalus_users:get_userspec(Config, alice)],
  542:     {ok, _Alice1, _} = escalus_connection:start(UserSpec),
  543:     [C2SPid] = ranch_procs(),
  544:     ok = rpc(mim(), sys, suspend, [C2SPid]),
  545: 
  546:     % WHEN a session gets replaced on a different node
  547:     UserSpec2 = [{port, 5232} | UserSpec],
  548:     {ok, Alice2, _} = escalus_connection:start(UserSpec2),
  549: 
  550:     % THEN a timeout warning is logged
  551:     FilterFun = fun(_, Msg) ->
  552:                         re:run(Msg, "replaced_wait_timeout") /= nomatch
  553:                 end,
  554:     mongoose_helper:wait_until(
  555:       fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1),
  556: 
  557:     rpc(mim(), sys, resume, [C2SPid]),
  558:     logger_ct_backend:stop_capture(),
  559: 
  560:     escalus_connection:stop(Alice2).
  561: 
  562: return_proper_stream_error_if_service_is_not_hidden(_Config) ->
  563:     % GIVEN MongooseIM is running default configuration
  564:     % WHEN we send non-XMPP payload
  565:     % THEN the server replies with stream error xml-not-well-formed and closes the connection
  566:     SendMalformedDataStep = fun(Client, Features) ->
  567:                                     escalus_connection:send_raw(Client, <<"malformed">>),
  568:                                     {Client, Features}
  569:                             end,
  570:     {ok, Connection, _} = escalus_connection:start([], [SendMalformedDataStep]),
  571:     escalus_connection:receive_stanza(Connection, #{ assert => is_stream_start }),
  572:     StreamErrorAssertion = {is_stream_error, [<<"xml-not-well-formed">>, <<>>]},
  573:     escalus_connection:receive_stanza(Connection, #{ assert => StreamErrorAssertion }),
  574:     %% Sometimes escalus needs a moment to report the connection as closed
  575:     escalus_connection:wait_for_close(Connection, 5000).
  576: 
  577: close_connection_if_service_type_is_hidden(_Config) ->
  578:     % GIVEN the option to hide service name is enabled
  579:     % WHEN we send non-XMPP payload
  580:     % THEN connection is closed without any response from the server
  581:     FailIfAnyDataReturned = fun(Reply) ->
  582:                                     ct:fail({unexpected_data, Reply})
  583:                             end,
  584:     Connection = escalus_tcp:connect(#{ on_reply => FailIfAnyDataReturned }),
  585:     Ref = monitor(process, Connection),
  586:     escalus_tcp:send(Connection, <<"malformed">>),
  587:     receive
  588:         {'DOWN', Ref, _, _, _} -> ok
  589:     after
  590:         5000 ->
  591:             ct:fail(connection_not_closed)
  592:     end.
  593: 
  594: close_connection_if_start_stream_duplicated(Config) ->
  595:     close_connection_if_protocol_violation(Config, [start_stream, stream_features]).
  596: 
  597: close_connection_if_protocol_violation_after_authentication(Config) ->
  598:     close_connection_if_protocol_violation(Config, [start_stream, stream_features, authenticate]).
  599: 
  600: close_connection_if_protocol_violation_after_binding(Config) ->
  601:     close_connection_if_protocol_violation(Config, [start_stream, stream_features, authenticate, bind]).
  602: 
  603: close_connection_if_protocol_violation(Config, Steps) ->
  604:     AliceSpec = escalus_fresh:create_fresh_user(Config, alice),
  605:     {ok, Alice, _Features} = escalus_connection:start(AliceSpec, Steps),
  606:     escalus:send(Alice, escalus_stanza:stream_start(domain(), ?NS_JABBER_CLIENT)),
  607:     escalus:assert(is_stream_error, [<<"policy-violation">>, <<>>],
  608:                    escalus_connection:get_stanza(Alice, no_stream_error_stanza_received)),
  609:     escalus:assert(is_stream_end,
  610:                    escalus_connection:get_stanza(Alice, no_stream_end_stanza_received)),
  611:     true = escalus_connection:wait_for_close(Alice,timer:seconds(5)).
  612: 
  613: cannot_connect_with_proxy_header(Config) ->
  614:     %% GIVEN proxy protocol is disabled
  615:     UserSpec = escalus_users:get_userspec(Config, alice),
  616: 
  617:     %% WHEN
  618:     ConnectionSteps = [{?MODULE, send_proxy_header}, start_stream],
  619:     {ok, ConnResult, _} = escalus_connection:start(UserSpec, ConnectionSteps),
  620: 
  621:     StreamError = escalus:wait_for_stanza(ConnResult),
  622:     escalus:assert(is_stream_error, [<<"xml-not-well-formed">>, <<>>], StreamError),
  623:     escalus_connection:stop(ConnResult).
  624: 
  625: cannot_connect_without_proxy_header(Config) ->
  626:     %% GIVEN proxy protocol is enabled
  627:     UserSpec = escalus_users:get_userspec(Config, alice),
  628: 
  629:     %% WHEN
  630:     ConnResult = escalus_connection:start(UserSpec, [start_stream]),
  631: 
  632:     %% THEN
  633:     ?assertMatch({error, {connection_step_failed, _, _}}, ConnResult).
  634: 
  635: connect_with_proxy_header(Config) ->
  636:     %% GIVEN proxy protocol is enabled
  637:     UserSpec = escalus_users:get_userspec(Config, alice),
  638: 
  639:     %% WHEN
  640:     ConnectionSteps = [{?MODULE, send_proxy_header}, start_stream, stream_features,
  641:                        authenticate, bind, session],
  642:     {ok, Conn, _Features} = escalus_connection:start(UserSpec, ConnectionSteps),
  643:     % make sure the session is present
  644:     escalus:send(Conn, escalus_stanza:presence(<<"available">>)),
  645:     escalus:assert(is_presence, escalus:wait_for_stanza(Conn)),
  646: 
  647:     %% THEN
  648:     SessionInfo = mongoose_helper:get_session_info(mim(), Conn),
  649:     #{src_address := IPAddr, src_port := Port} = proxy_info(),
  650:     ?assertMatch({IPAddr, Port}, maps:get(ip, SessionInfo)),
  651:     escalus_connection:stop(Conn).
  652: 
  653: %%--------------------------------------------------------------------
  654: %% Internal functions
  655: %%--------------------------------------------------------------------
  656: 
  657: c2s_port(Config) ->
  658:     case ?config(c2s_port, Config) of
  659:         undefined -> ct:get_config({hosts, mim, c2s_tls_port});
  660:         Value -> Value
  661:     end.
  662: 
  663: get_node(Port) ->
  664:     Mim2Port = ct:get_config({hosts, mim2, c2s_tls_port}),
  665:     Mim3Port = ct:get_config({hosts, mim3, c2s_tls_port}),
  666:     case Port of
  667:         Mim2Port ->
  668:             mim2();
  669:         Mim3Port ->
  670:             mim3();
  671:         _ ->
  672:             mim()
  673:     end.
  674: 
  675: ciphers_available_in_os() ->
  676:     CiphersStr = os:cmd("openssl ciphers 'ALL:eNULL'"),
  677:     [string:strip(C, both, $\n) || C <- string:tokens(CiphersStr, ":")].
  678: 
  679: ciphers_working_with_ssl_clients(Config) ->
  680:     Port = c2s_port(Config),
  681:     Path = rpc(get_node(Port), os, getenv, ["PWD"]),
  682:     CertPath = Path ++ "/" ++ ?CERT_FILE,
  683:     lists:filter(fun(Cipher) ->
  684:                          openssl_client_can_use_cipher(Cipher, Port, CertPath)
  685:                  end, ciphers_available_in_os()).
  686: 
  687: openssl_client_can_use_cipher(Cipher, Port, Path) ->
  688:     PortStr = integer_to_list(Port),
  689:     Cmd = "echo '' | openssl s_client -connect localhost:" ++ PortStr ++
  690:           " -cert \"" ++ Path ++ "\""
  691:           " -cipher \"" ++ Cipher ++ "\" -tls1_2 2>&1",
  692:     Output = os:cmd(Cmd),
  693:     0 == string:str(Output, ":error:") andalso 0 == string:str(Output, "errno=0").
  694: 
  695: restore_c2s_listener(Config) ->
  696:     C2SListener = ?config(c2s_listener, Config),
  697:     mongoose_helper:restart_listener(mim(), C2SListener).
  698: 
  699: assert_cert_file_exists() ->
  700:     ejabberd_node_utils:file_exists(?CERT_FILE) orelse
  701:         ct:fail("cert file ~s not exists", [?CERT_FILE]).
  702: 
  703: configure_c2s_listener(Config, ExtraC2sOpts) ->
  704:     configure_c2s_listener(Config, ExtraC2sOpts, []).
  705: 
  706: configure_c2s_listener(Config, ExtraC2SOpts, RemovedC2SKeys) ->
  707:     C2SListener = ?config(c2s_listener, Config),
  708:     NewC2SListener = maps:without(RemovedC2SKeys, maps:merge(C2SListener, ExtraC2SOpts)),
  709:     ct:pal("C2S listener: ~p", [NewC2SListener]),
  710:     mongoose_helper:restart_listener(mim(), NewC2SListener).
  711: 
  712: tls_opts(Mode, Config) ->
  713:     ExtraOpts = #{mode => Mode, cacertfile => ?CACERT_FILE, certfile => ?CERT_FILE, dhfile => ?DH_FILE},
  714:     Module = proplists:get_value(tls_module, Config, fast_tls),
  715:     maps:merge(default_c2s_tls(Module), ExtraOpts).
  716: 
  717: set_secure_connection_protocol(UserSpec, Version) ->
  718:     [{ssl_opts, [{versions, [Version]}, {verify, verify_none}]} | UserSpec].
  719: 
  720: connect_to_invalid_host(Spec) ->
  721:     {ok, Conn, _} = escalus_connection:start(Spec, [{?MODULE, connect_to_invalid_host}]),
  722:     escalus:wait_for_stanzas(Conn, 3).
  723: 
  724: connect_to_invalid_host(Conn, UnusedFeatures) ->
  725:     escalus:send(Conn, escalus_stanza:stream_start(<<"hopefullynonexistentdomain">>,
  726:                                                    ?NS_JABBER_CLIENT)),
  727:     {Conn, UnusedFeatures}.
  728: 
  729: connect_with_bad_xml(Spec) ->
  730:     {ok, Conn, _} = escalus_connection:start(Spec, [{?MODULE, connect_with_bad_xml}]),
  731:     escalus:wait_for_stanzas(Conn, 3).
  732: 
  733: connect_with_bad_xml(Conn, UnusedFeatures) ->
  734:     escalus_connection:send(Conn, #xmlcdata{content = "asdf\n"}),
  735:     {Conn, UnusedFeatures}.
  736: 
  737: connect_with_invalid_stream_namespace(Spec) ->
  738:     F = fun (Conn, UnusedFeatures) ->
  739:                 Start = stream_start_invalid_stream_ns(escalus_users:get_server([], Spec)),
  740:                 escalus:send(Conn, Start),
  741:                 {Conn, UnusedFeatures}
  742:         end,
  743:     {ok, Conn, _} = escalus_connection:start(Spec, [F]),
  744:     escalus:wait_for_stanzas(Conn, 3).
  745: 
  746: start_stream_pre_xmpp_1_0(Conn = #client{props = Props}, UnusedFeatures) ->
  747:     escalus:send(Conn, stream_start_pre_xmpp_1_0(escalus_users:get_server([], Props))),
  748:     #xmlstreamstart{attrs = StreamAttrs} = StreamStart = escalus:wait_for_stanza(Conn),
  749:     escalus:assert(is_stream_start, StreamStart),
  750:     {<<"id">>, StreamID} = lists:keyfind(<<"id">>, 1, StreamAttrs),
  751:     {Conn#client{props = [{stream_id, StreamID} | Props]}, UnusedFeatures}.
  752: 
  753: stream_start_pre_xmpp_1_0(To) ->
  754:         stream_start(lists:keystore(version, 1, default_context(To), {version, <<>>})).
  755: 
  756: stream_start(Context) ->
  757:     %% Be careful! The closing slash here is a hack to enable implementation of from_template/2
  758:     %% to parse the snippet properly. In standard XMPP <stream:stream> is just opening of an XML
  759:     %% element, NOT A SELF CLOSING element.
  760:     T = <<"<stream:stream {{version}} xml:lang='en' xmlns='jabber:client' "
  761:           "               to='{{to}}' "
  762:           "               xmlns:stream='{{stream_ns}}' />">>,
  763:     %% So we rewrap the parsed contents from #xmlel{} to #xmlstreamstart{} here.
  764:     #xmlel{name = Name, attrs = Attrs, children = []} = escalus_stanza:from_template(T, Context),
  765:     #xmlstreamstart{name = Name, attrs = Attrs}.
  766: 
  767: stream_start_invalid_stream_ns(To) ->
  768:     stream_start(lists:keystore(stream_ns, 1, default_context(To),
  769:                                 {stream_ns, <<"obviously-invalid-namespace">>})).
  770: 
  771: default_context(To) ->
  772:     [{version, <<"version='1.0'">>},
  773:      {to, To},
  774:      {stream_ns, ?NS_XMPP}].
  775: 
  776: children_specs_to_pids(Children) ->
  777:     [Pid || {_, Pid, _, _} <- Children].
  778: 
  779: ranch_procs() ->
  780:     Listeners = maps:keys(rpc(mim(), ranch, info, [])),
  781:     lists:foldl(
  782:         fun(Listener, Acc) -> rpc(mim(), ranch, procs, [Listener, connections]) ++ Acc end,
  783:         [],
  784:         Listeners).
  785: 
  786: pipeline_connect(UserSpec) ->
  787:     Server = proplists:get_value(server, UserSpec),
  788:     Username = proplists:get_value(username, UserSpec),
  789:     Password = proplists:get_value(password, UserSpec),
  790:     AuthPayload = <<0:8, Username/binary, 0:8, Password/binary>>,
  791: 
  792:     Conn = escalus_connection:connect(UserSpec),
  793: 
  794:     Stream = escalus_stanza:stream_start(Server, <<"jabber:client">>),
  795:     Auth = escalus_stanza:auth(<<"PLAIN">>, [#xmlcdata{content = base64:encode(AuthPayload)}]),
  796:     AuthStream = escalus_stanza:stream_start(Server, <<"jabber:client">>),
  797:     Bind = escalus_stanza:bind(<<?MODULE_STRING "_resource">>),
  798:     Session = escalus_stanza:session(),
  799: 
  800:     escalus_connection:send(Conn, [Stream, Auth, AuthStream, Bind, Session]),
  801:     Conn.
  802: 
  803: send_proxy_header(Conn, UnusedFeatures) ->
  804:     Header = ranch_proxy_header:header(proxy_info()),
  805:     escalus_connection:send_raw(Conn, iolist_to_binary(Header)),
  806:     {Conn, UnusedFeatures}.
  807: 
  808: proxy_info() ->
  809:     #{version => 2,
  810:       command => proxy,
  811:       transport_family => ipv4,
  812:       transport_protocol => stream,
  813:       src_address => {1, 2, 3, 4},
  814:       src_port => 444,
  815:       dest_address => {192, 168, 0, 1},
  816:       dest_port => 443
  817:      }.