diff options
Diffstat (limited to 'test/ejabberd_SUITE.erl')
-rw-r--r-- | test/ejabberd_SUITE.erl | 638 |
1 files changed, 638 insertions, 0 deletions
diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl new file mode 100644 index 000000000..0d6659639 --- /dev/null +++ b/test/ejabberd_SUITE.erl @@ -0,0 +1,638 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2013, Evgeniy Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 2 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(ejabberd_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include("ns.hrl"). +-include("ejabberd.hrl"). +-include("xmpp_codec.hrl"). + +-define(STREAM_HEADER, + <<"<?xml version='1.0'?><stream:stream " + "xmlns:stream='http://etherx.jabber.org/stream" + "s' xmlns='jabber:client' to='~s' version='1.0" + "'>">>). + +-define(STREAM_TRAILER, <<"</stream:stream>">>). + +-define(PUBSUB(Node), <<(?NS_PUBSUB)/binary, "#", Node>>). + +suite() -> + [{timetrap,{seconds,30}}]. + +init_per_suite(Config) -> + DataDir = proplists:get_value(data_dir, Config), + PrivDir = proplists:get_value(priv_dir, Config), + ConfigPath = filename:join([DataDir, "ejabberd.cfg"]), + LogPath = filename:join([PrivDir, "ejabberd.log"]), + SASLPath = filename:join([PrivDir, "sasl.log"]), + MnesiaDir = filename:join([PrivDir, "mnesia"]), + application:set_env(ejabberd, config, ConfigPath), + application:set_env(ejabberd, log_path, LogPath), + application:set_env(sasl, sasl_error_logger, {file, SASLPath}), + application:set_env(mnesia, dir, MnesiaDir), + [{server, <<"localhost">>}, + {port, 5222}, + {user, <<"test_suite">>}, + {password, <<"pass">>} + |Config]. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(start_ejabberd, Config) -> + Config; +init_per_testcase(TestCase, OrigConfig) -> + Resource = list_to_binary(atom_to_list(TestCase)), + Config = [{resource, Resource}|OrigConfig], + case TestCase of + test_connect -> + Config; + test_auth -> + connect(Config); + auth_md5 -> + connect(Config); + auth_plain -> + connect(Config); + test_bind -> + auth(connect(Config)); + test_open_session -> + bind(auth(connect(Config))); + _ -> + open_session(bind(auth(connect(Config)))) + end. + +end_per_testcase(_TestCase, _Config) -> + ok. + +groups() -> + []. + +%%all() -> [start_ejabberd, pubsub]. + +all() -> + [start_ejabberd, + test_connect, + auth_plain, + auth_md5, + test_auth, + test_bind, + test_open_session, + roster_get, + presence_broadcast, + ping, + version, + time, + stats, + disco, + last, + private, + privacy, + blocking, + vcard, + pubsub, + stop_ejabberd]. + +start_ejabberd(Config) -> + ok = application:start(ejabberd), + ok = re_register(Config), + Config. + +stop_ejabberd(Config) -> + ok = application:stop(ejabberd), + #stream_error{reason = 'system-shutdown'} = recv(), + {xmlstreamend, <<"stream:stream">>} = recv(), + Config. + +test_connect(Config) -> + disconnect(connect(Config)). + +connect(Config) -> + {ok, Sock} = ejabberd_socket:connect( + binary_to_list(?config(server, Config)), + ?config(port, Config), + [binary, {packet, 0}, {active, false}]), + Config1 = [{socket, Sock}|Config], + ok = send_text(Config1, io_lib:format(?STREAM_HEADER, + [?config(server, Config1)])), + {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), + <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs), + <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs), + #stream_features{sub_els = Fs} = recv(), + Mechs = lists:flatmap( + fun(#sasl_mechanisms{mechanism = Ms}) -> + Ms; + (_) -> + [] + end, Fs), + [{mechs, Mechs}|Config1]. + +disconnect(Config) -> + Socket = ?config(socket, Config), + ok = ejabberd_socket:send(Socket, ?STREAM_TRAILER), + {xmlstreamend, <<"stream:stream">>} = recv(), + ejabberd_socket:close(Socket), + Config. + +test_auth(Config) -> + disconnect(auth(Config)). + +auth(Config) -> + Mechs = ?config(mechs, Config), + HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs), + HavePLAIN = lists:member(<<"PLAIN">>, Mechs), + if HavePLAIN -> + auth_SASL(<<"PLAIN">>, Config); + HaveMD5 -> + auth_SASL(<<"DIGEST-MD5">>, Config); + true -> + ct:fail(no_sasl_mechanisms_available) + end. + +test_bind(Config) -> + disconnect(bind(Config)). + +bind(Config) -> + ID = send(Config, + #iq{type = set, + sub_els = [#bind{resource = ?config(resource, Config)}]}), + #iq{type = result, id = ID, sub_els = [#bind{}]} = recv(), + Config. + +test_open_session(Config) -> + disconnect(open_session(Config)). + +open_session(Config) -> + ID = send(Config, #iq{type = set, sub_els = [#session{}]}), + #iq{type = result, id = ID, sub_els = SubEls} = recv(), + case SubEls of + [] -> + ok; + [#session{}] -> + %% BUG: we should not receive this! + %% TODO: should be fixed in ejabberd + ok + end, + Config. + +roster_get(Config) -> + ID = send(Config, #iq{type = get, sub_els = [#roster{}]}), + #iq{type = result, id = ID, + sub_els = [#roster{item = []}]} = recv(), + disconnect(Config). + +presence_broadcast(Config) -> + send(Config, #presence{}), + JID = my_jid(Config), + #presence{from = JID, to = JID} = recv(), + disconnect(Config). + +ping(Config) -> + true = is_feature_advertised(Config, ?NS_PING), + ID = send(Config, + #iq{type = get, sub_els = [#ping{}], to = server_jid(Config)}), + #iq{type = result, id = ID, sub_els = []} = recv(), + disconnect(Config). + +version(Config) -> + true = is_feature_advertised(Config, ?NS_VERSION), + ID = send(Config, #iq{type = get, sub_els = [#version{}], + to = server_jid(Config)}), + #iq{type = result, id = ID, sub_els = [#version{}]} = recv(), + disconnect(Config). + +time(Config) -> + true = is_feature_advertised(Config, ?NS_TIME), + ID = send(Config, #iq{type = get, sub_els = [#time{}], + to = server_jid(Config)}), + #iq{type = result, id = ID, sub_els = [#time{}]} = recv(), + disconnect(Config). + +disco(Config) -> + true = is_feature_advertised(Config, ?NS_DISCO_INFO), + true = is_feature_advertised(Config, ?NS_DISCO_ITEMS), + I1 = send(Config, #iq{type = get, sub_els = [#disco_items{}], + to = server_jid(Config)}), + #iq{type = result, id = I1, sub_els = [#disco_items{items = Items}]} = recv(), + lists:foreach( + fun(#disco_item{jid = JID, node = Node}) -> + I = send(Config, + #iq{type = get, to = JID, + sub_els = [#disco_info{node = Node}]}), + #iq{type = result, id = I, sub_els = _} = recv() + end, Items), + disconnect(Config). + +private(Config) -> + I1 = send(Config, #iq{type = get, sub_els = [#private{}], + to = server_jid(Config)}), + #iq{type = error, id = I1} = recv(), + Conference = #bookmark_conference{name = <<"Some name">>, + autojoin = true, + jid = jlib:make_jid( + <<"some">>, + <<"some.conference.org">>, + <<>>)}, + Storage = #bookmark_storage{conference = [Conference]}, + I2 = send(Config, #iq{type = set, + sub_els = [#private{sub_els = [Storage]}]}), + #iq{type = result, id = I2, sub_els = []} = recv(), + I3 = send(Config, + #iq{type = get, + sub_els = [#private{sub_els = [#bookmark_storage{}]}]}), + #iq{type = result, id = I3, + sub_els = [#private{sub_els = [Storage]}]} = recv(), + disconnect(Config). + +last(Config) -> + true = is_feature_advertised(Config, ?NS_LAST), + ID = send(Config, #iq{type = get, sub_els = [#last{}], + to = server_jid(Config)}), + #iq{type = result, id = ID, sub_els = [#last{}]} = recv(), + disconnect(Config). + +privacy(Config) -> + %% BUG: the feature MUST be advertised via disco#info: + %% http://xmpp.org/extensions/xep-0016.html#disco + %% It seems like this bug exists because Privacy Lists + %% were implemented according to the old RFC where support + %% needn't be advertised via service discovery. + %% TODO: fix in ejabberd + %% true = is_feature_advertised(Config, ?NS_PRIVACY), + I1 = send(Config, #iq{type = get, sub_els = [#privacy{}]}), + #iq{type = result, id = I1, sub_els = [#privacy{}]} = recv(), + JID = <<"tybalt@example.com">>, + I2 = send(Config, + #iq{type = set, + sub_els = [#privacy{ + list = [#privacy_list{ + name = <<"public">>, + privacy_item = + [#privacy_item{ + type = jid, + order = 3, + action = deny, + stanza = 'presence-in', + value = JID}]}]}]}), + #iq{type = result, id = I2, sub_els = []} = recv(), + _Push1 = #iq{type = set, id = PushI1, + sub_els = [#privacy{ + list = [#privacy_list{ + name = <<"public">>}]}]} = recv(), + %% BUG: ejabberd replies on this result + %% TODO: this should be fixed in ejabberd + %% _ = send(Config, Push1#iq{type = result, sub_els = []}), + I3 = send(Config, #iq{type = set, + sub_els = [#privacy{active = <<"public">>}]}), + #iq{type = result, id = I3, sub_els = []} = recv(), + I4 = send(Config, #iq{type = set, + sub_els = [#privacy{default = <<"public">>}]}), + #iq{type = result, id = I4, sub_els = []} = recv(), + I5 = send(Config, #iq{type = get, sub_els = [#privacy{}]}), + #iq{type = result, id = I5, + sub_els = [#privacy{default = <<"public">>, + active = <<"public">>, + list = [#privacy_list{name = <<"public">>}]}]} = recv(), + I6 = send(Config, + #iq{type = set, sub_els = [#privacy{default = none}]}), + #iq{type = result, id = I6, sub_els = []} = recv(), + I7 = send(Config, #iq{type = set, sub_els = [#privacy{active = none}]}), + #iq{type = result, id = I7, sub_els = []} = recv(), + I8 = send(Config, #iq{type = set, + sub_els = [#privacy{ + list = + [#privacy_list{ + name = <<"public">>}]}]}), + #iq{type = result, id = I8, sub_els = []} = recv(), + %% BUG: We should receive this: + %% _Push2 = #iq{type = set, id = PushI2, sub_els = []} = recv(), + %% TODO: this should be fixed in ejabberd + _Push2 = #iq{type = set, id = PushI2, + sub_els = [#privacy{ + list = [#privacy_list{ + name = <<"public">>}]}]} = recv(), + disconnect(Config). + +blocking(Config) -> + true = is_feature_advertised(Config, ?NS_BLOCKING), + JID = jlib:make_jid(<<"romeo">>, <<"montague.net">>, <<>>), + I1 = send(Config, #iq{type = get, sub_els = [#block_list{}]}), + #iq{type = result, id = I1, sub_els = [#block_list{}]} = recv(), + I2 = send(Config, #iq{type = set, + sub_els = [#block{block_item = [JID]}]}), + #iq{type = result, id = I2, sub_els = []} = recv(), + #iq{type = set, id = _, + sub_els = [#privacy{list = [#privacy_list{}]}]} = recv(), + #iq{type = set, id = _, + sub_els = [#block{block_item = [JID]}]} = recv(), + I3 = send(Config, #iq{type = set, + sub_els = [#unblock{block_item = [JID]}]}), + #iq{type = result, id = I3, sub_els = []} = recv(), + #iq{type = set, id = _, + sub_els = [#privacy{list = [#privacy_list{}]}]} = recv(), + #iq{type = set, id = _, + sub_els = [#unblock{block_item = [JID]}]} = recv(), + disconnect(Config). + +vcard(Config) -> + true = is_feature_advertised(Config, ?NS_VCARD), + VCard = + #vcard{fn = <<"Peter Saint-Andre">>, + n = #vcard_name{family = <<"Saint-Andre">>, + given = <<"Peter">>}, + nickname = <<"stpeter">>, + bday = <<"1966-08-06">>, + adr = [#vcard_adr{work = true, + extadd = <<"Suite 600">>, + street = <<"1899 Wynkoop Street">>, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80202">>, + ctry = <<"USA">>}, + #vcard_adr{home = true, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80209">>, + ctry = <<"USA">>}], + tel = [#vcard_tel{work = true,voice = true, + number = <<"303-308-3282">>}, + #vcard_tel{home = true,voice = true, + number = <<"303-555-1212">>}], + email = [#vcard_email{internet = true,pref = true, + userid = <<"stpeter@jabber.org">>}], + jabberid = <<"stpeter@jabber.org">>, + title = <<"Executive Director">>,role = <<"Patron Saint">>, + org = #vcard_org{name = <<"XMPP Standards Foundation">>}, + url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, + desc = <<"More information about me is located on my " + "personal website: http://www.saint-andre.com/">>}, + I1 = send(Config, #iq{type = set, sub_els = [VCard]}), + #iq{type = result, id = I1, sub_els = []} = recv(), + I2 = send(Config, #iq{type = get, sub_els = [#vcard{}]}), + %% TODO: check if VCard == VCard1. + #iq{type = result, id = I2, sub_els = [_VCard1]} = recv(), + disconnect(Config). + +stats(Config) -> + ServerJID = server_jid(Config), + ID = send(Config, #iq{type = get, sub_els = [#stats{}], + to = server_jid(Config)}), + #iq{type = result, id = ID, sub_els = [#stats{stat = Stats}]} = recv(), + lists:foreach( + fun(#stat{name = Name} = Stat) -> + I = send(Config, #iq{type = get, + sub_els = [#stats{stat = [Stat]}], + to = server_jid(Config)}), + #iq{type = result, id = I, sub_els = [_|_]} = recv() + end, Stats), + disconnect(Config). + +pubsub(Config) -> + true = is_feature_advertised(Config, ?NS_PUBSUB), + %% Get subscriptions + %% true = is_feature_advertised(Config, ?PUBSUB("retrieve-subscriptions")), + %% I1 = send(Config, #iq{type = get, to = pubsub_jid(Config), + %% sub_els = [#pubsub{subscriptions = {none, []}}]}), + %% #iq{type = result, id = I1, + %% sub_els = [#pubsub{subscriptions = {none, []}}]} = recv(), + %% %% Get affiliations + %% true = is_feature_advertised(Config, ?PUBSUB("retrieve-affiliations")), + %% I2 = send(Config, #iq{type = get, to = pubsub_jid(Config), + %% sub_els = [#pubsub{affiliations = []}]}), + %% #iq{type = result, id = I2, + %% sub_els = [#pubsub{affiliations = []}]} = recv(), + + true = is_feature_advertised(Config, ?NS_PUBSUB), + %% Publish <presence/> element within node "presence" + ItemID = randoms:get_string(), + Node = <<"presence">>, + Item = #pubsub_item{id = ItemID, sub_els = [#presence{}]}, + I1 = send(Config, + #iq{type = set, to = pubsub_jid(Config), + sub_els = [#pubsub{publish = {Node, [Item]}}]}), + #iq{type = result, id = I1, + sub_els = [#pubsub{publish = {<<"presence">>, + [#pubsub_item{id = ItemID}]}}]} = recv(), + %% Subscribe to node "presence" + I2 = send(Config, + #iq{type = set, to = pubsub_jid(Config), + sub_els = [#pubsub{subscribe = {Node, my_jid(Config)}}]}), + #message{sub_els = [#pubsub_event{}, #delay{}]} = recv(), + #iq{type = result, id = I2} = recv(), + disconnect(Config). + +auth_md5(Config) -> + Mechs = ?config(mechs, Config), + case lists:member(<<"DIGEST-MD5">>, Mechs) of + true -> + disconnect(auth_SASL(<<"DIGEST-MD5">>, Config)); + false -> + disconnect(Config), + {skipped, 'DIGEST-MD5_not_available'} + end. + +auth_plain(Config) -> + Mechs = ?config(mechs, Config), + case lists:member(<<"PLAIN">>, Mechs) of + true -> + disconnect(auth_SASL(<<"PLAIN">>, Config)); + false -> + disconnect(Config), + {skipped, 'PLAIN_not_available'} + end. + +auth_SASL(Mech, Config) -> + {Response, SASL} = sasl_new(Mech, + ?config(user, Config), + ?config(server, Config), + ?config(password, Config)), + send(Config, #sasl_auth{mechanism = Mech, cdata = Response}), + wait_auth_SASL_result([{sasl, SASL}|Config]). + +wait_auth_SASL_result(Config) -> + case recv() of + #sasl_success{} -> + ejabberd_socket:reset_stream(?config(socket, Config)), + send_text(Config, + io_lib:format(?STREAM_HEADER, + [?config(server, Config)])), + {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), + <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs), + <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs), + #stream_features{} = recv(), + Config; + #sasl_challenge{cdata = ClientIn} -> + {Response, SASL} = (?config(sasl, Config))(ClientIn), + send(Config, #sasl_response{cdata = Response}), + Config1 = proplists:delete(sasl, Config), + wait_auth_SASL_result([{sasl, SASL}|Config1]); + #sasl_failure{} -> + ct:fail(sasl_auth_failed) + end. + +%%%=================================================================== +%%% Aux functions +%%%=================================================================== +re_register(Config) -> + User = ?config(user, Config), + Server = ?config(server, Config), + Pass = ?config(password, Config), + {atomic, ok} = ejabberd_auth:try_register(User, Server, Pass), + ok. + +recv() -> + receive + {'$gen_event', {xmlstreamelement, El}} -> + ct:log("recv: ~p", [El]), + xmpp_codec:decode(El); + {'$gen_event', Event} -> + Event + end. + +send_text(Config, Text) -> + ejabberd_socket:send(?config(socket, Config), Text). + +send(State, Pkt) -> + {NewID, NewPkt} = case Pkt of + #message{id = I} -> + ID = id(I), + {ID, Pkt#message{id = ID}}; + #presence{id = I} -> + ID = id(I), + {ID, Pkt#presence{id = ID}}; + #iq{id = I} -> + ID = id(I), + {ID, Pkt#iq{id = ID}}; + _ -> + {undefined, Pkt} + end, + El = xmpp_codec:encode(NewPkt), + ct:log("sent: ~p", [El]), + ok = send_text(State, xml:element_to_binary(El)), + NewID. + +sasl_new(<<"PLAIN">>, User, Server, Password) -> + {<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>, + fun (_) -> {error, <<"Invalid SASL challenge">>} end}; +sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> + {<<"">>, + fun (ServerIn) -> + case cyrsasl_digest:parse(ServerIn) of + bad -> {error, <<"Invalid SASL challenge">>}; + KeyVals -> + Nonce = xml:get_attr_s(<<"nonce">>, KeyVals), + CNonce = id(), + DigestURI = <<"xmpp/", Server/binary>>, + Realm = Server, + NC = <<"00000001">>, + QOP = <<"auth">>, + AuthzId = <<"">>, + MyResponse = response(User, Password, Nonce, AuthzId, + Realm, CNonce, DigestURI, NC, QOP, + <<"AUTHENTICATE">>), + ServerResponse = response(User, Password, Nonce, + AuthzId, Realm, CNonce, DigestURI, + NC, QOP, <<"">>), + Resp = <<"username=\"", User/binary, "\",realm=\"", + Realm/binary, "\",nonce=\"", Nonce/binary, + "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary, + ",qop=", QOP/binary, ",digest-uri=\"", + DigestURI/binary, "\",response=\"", + MyResponse/binary, "\"">>, + {Resp, + fun (ServerIn2) -> + case cyrsasl_digest:parse(ServerIn2) of + bad -> {error, <<"Invalid SASL challenge">>}; + KeyVals2 -> + RspAuth = xml:get_attr_s(<<"rspauth">>, + KeyVals2), + if RspAuth == ServerResponse -> + {<<"">>, + fun (_) -> + {error, + <<"Invalid SASL challenge">>} + end}; + true -> + {error, <<"Invalid SASL challenge">>} + end + end + end} + end + end}. + +hex(S) -> + sha:to_hexlist(S). + +response(User, Passwd, Nonce, AuthzId, Realm, CNonce, + DigestURI, NC, QOP, A2Prefix) -> + A1 = case AuthzId of + <<"">> -> + <<((crypto:md5(<<User/binary, ":", Realm/binary, ":", + Passwd/binary>>)))/binary, + ":", Nonce/binary, ":", CNonce/binary>>; + _ -> + <<((crypto:md5(<<User/binary, ":", Realm/binary, ":", + Passwd/binary>>)))/binary, + ":", Nonce/binary, ":", CNonce/binary, ":", + AuthzId/binary>> + end, + A2 = case QOP of + <<"auth">> -> + <<A2Prefix/binary, ":", DigestURI/binary>>; + _ -> + <<A2Prefix/binary, ":", DigestURI/binary, + ":00000000000000000000000000000000">> + end, + T = <<(hex((crypto:md5(A1))))/binary, ":", Nonce/binary, + ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, + ":", (hex((crypto:md5(A2))))/binary>>, + hex((crypto:md5(T))). + +my_jid(Config) -> + jlib:make_jid(?config(user, Config), + ?config(server, Config), + ?config(resource, Config)). + +server_jid(Config) -> + jlib:make_jid(<<>>, ?config(server, Config), <<>>). + +pubsub_jid(Config) -> + Server = ?config(server, Config), + jlib:make_jid(<<>>, <<"pubsub.", Server/binary>>, <<>>). + +id() -> + id(undefined). + +id(undefined) -> + randoms:get_string(); +id(ID) -> + ID. + +is_feature_advertised(Config, Feature) -> + ID = send(Config, #iq{type = get, sub_els = [#disco_info{}], + to = server_jid(Config)}), + #iq{type = result, id = ID, + sub_els = [#disco_info{feature = Features}]} = recv(), + lists:member(Feature, Features). + +bookmark_conference() -> + #bookmark_conference{name = <<"Some name">>, + autojoin = true, + jid = jlib:make_jid( + <<"some">>, + <<"some.conference.org">>, + <<>>)}. |