diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index fdcc7508c..53f02507f 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -40,6 +40,11 @@ send_element/2, socket_type/0, get_presence/1, + get_aux_field/2, + set_aux_field/3, + del_aux_field/2, + get_subscription/2, + broadcast/4, get_subscribed/1]). %% API: @@ -67,6 +72,7 @@ -export([get_state/1]). -include_lib("exmpp/include/exmpp.hrl"). +-include_lib("exmpp/include/exmpp_jid.hrl"). -include("ejabberd.hrl"). -include("mod_privacy.hrl"). @@ -106,6 +112,7 @@ conn = unknown, auth_module = unknown, ip, + aux_fields = [], fsm_limit_opts, lang}). @@ -182,6 +189,38 @@ socket_type() -> get_presence(FsmRef) -> ?GEN_FSM:sync_send_all_state_event(FsmRef, {get_presence}, 1000). +get_aux_field(Key, #state{aux_fields = Opts}) -> + case lists:keysearch(Key, 1, Opts) of + {value, {_, Val}} -> + {ok, Val}; + _ -> + error + end. + +set_aux_field(Key, Val, #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = [{Key, Val}|Opts1]}. + +del_aux_field(Key, #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = Opts1}. + +get_subscription(From = #jid{}, StateData) -> + get_subscription(exmpp_jid:to_lower(From), StateData); +get_subscription(LFrom, StateData) -> + LBFrom = setelement(3, LFrom, undefined), + F = ?SETS:is_element(LFrom, StateData#state.pres_f) orelse + ?SETS:is_element(LBFrom, StateData#state.pres_f), + T = ?SETS:is_element(LFrom, StateData#state.pres_t) orelse + ?SETS:is_element(LBFrom, StateData#state.pres_t), + if F and T -> both; + F -> from; + T -> to; + true -> none + end. + +broadcast(FsmRef, Type, From, Packet) -> + FsmRef ! {broadcast, Type, From, Packet}. %%TODO: for debug only get_state(FsmRef) -> @@ -1177,35 +1216,39 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> {Pass, NewAttrs, NewState} = case Packet of #xmlel{attrs = Attrs} when ?IS_PRESENCE(Packet) -> + State = ejabberd_hooks:run_fold( + c2s_presence_in, StateData#state.server, + StateData, + [{From, To, Packet}]), case exmpp_presence:get_type(Packet) of 'probe' -> LFrom = jlib:short_prepd_jid(From), LBFrom = jlib:short_prepd_bare_jid(From), NewStateData = case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse + LFrom, State#state.pres_a) orelse ?SETS:is_element( - LBFrom, StateData#state.pres_a) of + LBFrom, State#state.pres_a) of true -> - StateData; + State; false -> case ?SETS:is_element( - LFrom, StateData#state.pres_f) of + LFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; + State#state.pres_a), + State#state{pres_a = A}; false -> case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of + LBFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LBFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; + State#state.pres_a), + State#state{pres_a = A}; false -> - StateData + State end end end, @@ -1214,56 +1257,56 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> 'error' -> LFrom = jlib:short_prepd_jid(From), NewA = remove_element(LFrom, - StateData#state.pres_a), - {true, Attrs, StateData#state{pres_a = NewA}}; + State#state.pres_a), + {true, Attrs, State#state{pres_a = NewA}}; 'subscribe' -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; 'subscribed' -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; 'unsubscribe' -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; 'unsubscribed' -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; _ -> - case privacy_check_packet(StateData, From, To, Packet, in) of + case privacy_check_packet(State, From, To, Packet, in) of allow -> LFrom = jlib:short_prepd_jid(From), LBFrom = jlib:short_prepd_bare_jid(From), case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse + LFrom, State#state.pres_a) orelse ?SETS:is_element( - LBFrom, StateData#state.pres_a) of + LBFrom, State#state.pres_a) of true -> - {true, Attrs, StateData}; + {true, Attrs, State}; false -> case ?SETS:is_element( - LFrom, StateData#state.pres_f) of + LFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LFrom, - StateData#state.pres_a), + State#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + State#state{pres_a = A}}; false -> case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of + LBFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LBFrom, - StateData#state.pres_a), + State#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + State#state{pres_a = A}}; false -> - {true, Attrs, StateData} + {true, Attrs, State} end end end; deny -> - {false, Attrs, StateData} + {false, Attrs, State} end end; #xmlel{name = broadcast, attrs = Attrs} -> @@ -1403,6 +1446,15 @@ handle_info({force_update_presence, LUser}, StateName, StateData end, {next_state, StateName, NewStateData}; +handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> + Recipients = ejabberd_hooks:run_fold( + c2s_broadcast_recipients, StateData#state.server, + [], + [StateData, Type, From, Packet]), + lists:foreach(fun(USR) -> + ejabberd_router:route(From, exmpp_jid:make(USR), Packet) + end, lists:usort(Recipients)), + fsm_next_state(StateName, StateData); handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), fsm_next_state(StateName, StateData). diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 9c9518e64..f2f560829 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -52,16 +52,22 @@ code_change/3 ]). --include_lib("exmpp/include/exmpp.hrl"). - %% hook handlers --export([user_send_packet/3]). +-export([user_send_packet/3, + user_receive_packet/4, + c2s_presence_in/2, + c2s_broadcast_recipients/5]). + + +-include_lib("exmpp/include/exmpp.hrl"). +-include_lib("exmpp/include/exmpp_jid.hrl"). -include("ejabberd.hrl"). -define(PROCNAME, ejabberd_mod_caps). +-define(BAD_HASH_LIFETIME, 600). %% in seconds --record(caps, {node, version, exts}). +-record(caps, {node, version, hash, exts}). -record(caps_features, {node_pair, features = []}). -record(state, {host}). @@ -103,9 +109,11 @@ get_features(#caps{node = Node, version = Version, exts = Exts}) -> case cache_tab:lookup(caps_features, BinaryNode, caps_read_fun(BinaryNode)) of error -> - Acc; - {ok, Features} -> - binary_to_features(Features) ++ Acc + Acc; + {ok, Features} when is_list(Features) -> + binary_to_features(Features) ++ Acc; + _ -> + Acc end end, [], SubNodes). @@ -115,11 +123,13 @@ get_features(#caps{node = Node, version = Version, exts = Exts}) -> %% capabilities are advertised. read_caps(Els) -> read_caps(Els, nothing). + read_caps([#xmlel{ns = ?NS_CAPS, name = 'c'} = El | Tail], _Result) -> Node = exmpp_xml:get_attribute_as_list(El, <<"node">>, ""), Version = exmpp_xml:get_attribute_as_list(El, <<"ver">>, ""), + Hash = exmpp_xml:get_attribute_as_list(El, <<"hash">>, ""), Exts = string:tokens(exmpp_xml:get_attribute_as_list(El, <<"ext">>, ""), " "), - read_caps(Tail, #caps{node = Node, version = Version, exts = Exts}); + read_caps(Tail, #caps{node = Node, hash = Hash, version = Version, exts = Exts}); read_caps([#xmlel{ns = ?NS_MUC_USER, name = 'x'} | _Tail], _Result) -> nothing; read_caps([_ | Tail], Result) -> @@ -130,27 +140,36 @@ read_caps([], Result) -> %%==================================================================== %% Hooks %%==================================================================== -user_send_packet(From, To, #xmlel{name = 'presence', attrs = Attrs, children = Els}) -> - case exmpp_jid:bare_compare(From, To) of - true -> - Type = exmpp_xml:get_attribute_from_list_as_list(Attrs, <<"type">>, ""), - if Type == ""; Type == "available" -> - case read_caps(Els) of - nothing -> - ok; - #caps{version = Version, exts = Exts} = Caps -> - Server = exmpp_jid:prep_domain_as_list(From), - feature_request(Server, From, Caps, [Version | Exts]) +user_send_packet(#jid{node = U, domain = S} = From, #jid{node = U, domain = S} = _To, + #xmlel{name = 'presence', attrs = Attrs, children = Els}) -> + Type = exmpp_xml:get_attribute_from_list_as_list(Attrs, <<"type">>, ""), + if Type == ""; Type == "available" -> + case read_caps(Els) of + nothing -> + ok; + #caps{version = Version, exts = Exts} = Caps -> + feature_request(_Server = binary_to_list(S), From, Caps, [Version | Exts]) end; - true -> - ok - end; - false -> + true -> ok end; user_send_packet(_From, _To, _Packet) -> ok. +user_receive_packet(#jid{domain = Server}, From, _To, + #xmlel{name = 'presence', attrs = Attrs, children = Els}) -> + Type = exmpp_xml:get_attribute_from_list_as_list(Attrs, <<"type">>, ""), + if Type == ""; Type == "available" -> + case read_caps(Els) of + nothing -> + ok; + #caps{version = Version, exts = Exts} = Caps -> + feature_request(Server, From, Caps, [Version | Exts]) + end + end; +user_receive_packet(_JID, _From, _To, _Packet) -> + ok. + caps_stream_features(Acc, MyHost) -> case make_my_disco_hash(MyHost) of "" -> @@ -165,7 +184,7 @@ caps_stream_features(Acc, MyHost) -> disco_features(_Acc, From, To, <>, Lang) -> ejabberd_hooks:run_fold(disco_local_features, - exmpp_jid:domain(To), + To#jid.domain, empty, [From, To, <<>>, Lang]); disco_features(Acc, _From, _To, _Node, _Lang) -> @@ -173,7 +192,7 @@ disco_features(Acc, _From, _To, _Node, _Lang) -> disco_identity(_Acc, From, To, <>, Lang) -> ejabberd_hooks:run_fold(disco_local_identity, - exmpp_jid:domain(To), + To#jid.domain, [], [From, To, <<>>, Lang]); disco_identity(Acc, _From, _To, _Node, _Lang) -> @@ -187,6 +206,67 @@ disco_info(_Acc, Host, Module, <>, Lang) -> disco_info(Acc, _Host, _Module, _Node, _Lang) -> Acc. +c2s_presence_in(C2SState, {From, To, #xmlel{attrs = Attrs, children = Els}}) -> + Type = exmpp_xml:get_attribute_from_list_as_list(Attrs, <<"type">>, ""), + Subscription = ejabberd_c2s:get_subscription(From, C2SState), + Insert = ((Type == "") or (Type == "available")) + and ((Subscription == both) or (Subscription == to)), + Delete = (Type == "unavailable") or (Type == "error") or (Type == "invisible"), + if Insert or Delete -> + LFrom = exmpp_jid:to_lower(From), + Rs = case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of + {ok, Rs1} -> + Rs1; + error -> + gb_trees:empty() + end, + Caps = read_caps(Els), + {CapsUpdated, NewRs} = + case Caps of + nothing when Insert == true -> + {false, Rs}; + _ when Insert == true -> + case gb_trees:lookup(LFrom, Rs) of + {value, Caps} -> + {false, Rs}; + none -> + {true, gb_trees:insert(LFrom, Caps, Rs)}; + _ -> + {true, gb_trees:update(LFrom, Caps, Rs)} + end; + _ -> + {false, gb_trees:delete_any(LFrom, Rs)} + end, + if CapsUpdated -> + ejabberd_hooks:run(caps_update, To#jid.domain, + [From, To, get_features(Caps)]); + true -> + ok + end, + ejabberd_c2s:set_aux_field(caps_resources, NewRs, C2SState); + true -> + C2SState + end. + +c2s_broadcast_recipients(InAcc, C2SState, {pep_message, Feature}, + _From, _Packet) -> + case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of + {ok, Rs} -> + gb_trees_fold( + fun(USR, Caps, Acc) -> + case lists:member(Feature, get_features(Caps)) of + true -> + [USR|Acc]; + false -> + Acc + end + end, InAcc, Rs); + _ -> + InAcc + end; +c2s_broadcast_recipients(Acc, _, _, _, _) -> + Acc. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -208,7 +288,14 @@ init([Host, Opts]) -> LifeTime = gen_mod:get_opt(cache_life_time, Opts, timer:hours(24) div 1000), cache_tab:new(caps_features, [{max_size, MaxSize}, {life_time, LifeTime}]), HostB = list_to_binary(Host), - ejabberd_hooks:add(user_send_packet, HostB, ?MODULE, user_send_packet, 75), + ejabberd_hooks:add(c2s_presence_in, HostB, + ?MODULE, c2s_presence_in, 75), + ejabberd_hooks:add(c2s_broadcast_recipients, HostB, + ?MODULE, c2s_broadcast_recipients, 75), + ejabberd_hooks:add(user_send_packet, HostB, + ?MODULE, user_send_packet, 75), + ejabberd_hooks:add(user_receive_packet, HostB, + ?MODULE, user_receive_packet, 75), ejabberd_hooks:add(c2s_stream_features, HostB, ?MODULE, caps_stream_features, 75), ejabberd_hooks:add(s2s_stream_features, HostB, @@ -234,7 +321,14 @@ handle_info(_Info, State) -> terminate(_Reason, State) -> HostB = list_to_binary(State#state.host), - ejabberd_hooks:delete(user_send_packet, HostB, ?MODULE, user_send_packet, 75), + ejabberd_hooks:delete(c2s_presence_in, HostB, + ?MODULE, c2s_presence_in, 75), + ejabberd_hooks:delete(c2s_broadcast_recipients, HostB, + ?MODULE, c2s_broadcast_recipients, 75), + ejabberd_hooks:delete(user_send_packet, HostB, + ?MODULE, user_send_packet, 75), + ejabberd_hooks:delete(user_receive_packet, HostB, + ?MODULE, user_receive_packet, 75), ejabberd_hooks:delete(c2s_stream_features, HostB, ?MODULE, caps_stream_features, 75), ejabberd_hooks:delete(s2s_stream_features, HostB, @@ -258,45 +352,67 @@ feature_request(Host, From, Caps, [SubNode | Tail] = SubNodes) -> BinaryNode = node_to_binary(Node, SubNode), case cache_tab:lookup(caps_features, BinaryNode, caps_read_fun(BinaryNode)) of - error -> - IQ = #iq{type = get, - iq_ns = ?NS_JABBER_CLIENT, - payload = #xmlel{ns = ?NS_DISCO_INFO, name = 'query', - attrs = [?XMLATTR(<<"node">>, Node ++ "#" ++ SubNode)]}}, - F = fun(IQReply) -> - feature_response( - IQReply, Host, From, Caps, SubNodes) - end, - ejabberd_local:route_iq( - exmpp_jid:make("", Host, ""), From, IQ, F); - _ -> - feature_request(Host, From, Caps, Tail) + {ok, Fs} when is_list(Fs) -> + feature_request(Host, From, Caps, Tail); + Other -> + NeedRequest = case Other of + {ok, TS} -> + now_ts() >= TS + ?BAD_HASH_LIFETIME; + _ -> + true + end, + if NeedRequest -> + IQ = #iq{type = 'get', + ns = ?NS_DISCO_INFO, + payload = #xmlel{ns = ?NS_DISCO_INFO, + name = 'query', + attrs = [ + #xmlattr{name = <<"node">>, + value = list_to_binary( + Node ++ "#" ++ SubNode)}]}}, + F = fun(IQReply) -> + feature_response( + IQReply, Host, From, Caps, SubNodes) + end, + ejabberd_local:route_iq( + exmpp_jid:make(Host), From, IQ, F); + true -> + feature_request(Host, From, Caps, Tail) + end end; feature_request(_Host, _From, _Caps, []) -> ok. -feature_response(#iq{type = result, payload = El}, +feature_response(#iq{type = result, + payload = #xmlel{children = Els}}, Host, From, Caps, [SubNode | SubNodes]) -> - Features = lists:flatmap( + BinaryNode = node_to_binary(Caps#caps.node, SubNode), + case check_hash(Caps, Els) of + true -> + Features = lists:flatmap( fun(#xmlel{name = 'feature', attrs = FAttrs}) -> - [exmpp_xml:get_attribute_from_list_as_list(FAttrs, <<"var">>, "")]; + [exmpp_xml:get_attribute_from_list(FAttrs, <<"var">>, "")]; (_) -> [] - end, El#xmlel.children), - BinaryNode = node_to_binary(Caps#caps.node, SubNode), - BinaryFeatures = features_to_binary(Features), - cache_tab:insert( - caps_features, BinaryNode, BinaryFeatures, - caps_write_fun(BinaryNode, BinaryFeatures)), + end, Els), + BinaryFeatures = features_to_binary(Features), + cache_tab:insert( + caps_features, BinaryNode, BinaryFeatures, + caps_write_fun(BinaryNode, BinaryFeatures)); + false -> + %% We cache current timestamp and will probe the client + %% after BAD_HASH_LIFETIME seconds. + cache_tab:insert(caps_features, BinaryNode, now_ts(), + caps_write_fun(BinaryNode, now_ts())) + end, feature_request(Host, From, Caps, SubNodes); -feature_response(timeout, _Host, _From, _Caps, _SubNodes) -> - ok; feature_response(_IQResult, Host, From, Caps, [SubNode | SubNodes]) -> - %% We got type=error or invalid type=result stanza, so - %% we cache empty feature not to probe the client permanently + %% We got type=error or invalid type=result stanza or timeout, + %% so we cache current timestamp and will probe the client + %% after BAD_HASH_LIFETIME seconds. BinaryNode = node_to_binary(Caps#caps.node, SubNode), - cache_tab:insert(caps_features, BinaryNode, [], - caps_write_fun(BinaryNode, [])), + cache_tab:insert(caps_features, BinaryNode, now_ts(), + caps_write_fun(BinaryNode, now_ts())), feature_request(Host, From, Caps, SubNodes). node_to_binary(Node, SubNode) -> @@ -350,11 +466,85 @@ make_my_disco_hash(Host) -> "" end. -make_disco_hash(DiscoEls, Algo) when Algo == sha1 -> +-ifdef(HAVE_MD2). +make_disco_hash(DiscoEls, Algo) -> Concat = [concat_identities(DiscoEls), concat_features(DiscoEls), concat_info(DiscoEls)], - base64:encode_to_string(crypto:sha(Concat)). + base64:encode_to_string( + if Algo == md2 -> + sha:md2(Concat); + Algo == md5 -> + crypto:md5(Concat); + Algo == sha1 -> + crypto:sha(Concat); + Algo == sha224 -> + sha:sha224(Concat); + Algo == sha256 -> + sha:sha256(Concat); + Algo == sha384 -> + sha:sha384(Concat); + Algo == sha512 -> + sha:sha512(Concat) + end). + +check_hash(Caps, Els) -> + case Caps#caps.hash of + "md2" -> + Caps#caps.version == make_disco_hash(Els, md2); + "md5" -> + Caps#caps.version == make_disco_hash(Els, md5); + "sha-1" -> + Caps#caps.version == make_disco_hash(Els, sha1); + "sha-224" -> + Caps#caps.version == make_disco_hash(Els, sha224); + "sha-256" -> + Caps#caps.version == make_disco_hash(Els, sha256); + "sha-384" -> + Caps#caps.version == make_disco_hash(Els, sha384); + "sha-512" -> + Caps#caps.version == make_disco_hash(Els, sha512); + _ -> + true + end. +-else. +make_disco_hash(DiscoEls, Algo) -> + Concat = [concat_identities(DiscoEls), + concat_features(DiscoEls), + concat_info(DiscoEls)], + base64:encode_to_string( + if Algo == md5 -> + crypto:md5(Concat); + Algo == sha1 -> + crypto:sha(Concat); + Algo == sha224 -> + sha:sha224(Concat); + Algo == sha256 -> + sha:sha256(Concat); + Algo == sha384 -> + sha:sha384(Concat); + Algo == sha512 -> + sha:sha512(Concat) + end). + +check_hash(Caps, Els) -> + case Caps#caps.hash of + "md5" -> + Caps#caps.version == make_disco_hash(Els, md5); + "sha-1" -> + Caps#caps.version == make_disco_hash(Els, sha1); + "sha-224" -> + Caps#caps.version == make_disco_hash(Els, sha224); + "sha-256" -> + Caps#caps.version == make_disco_hash(Els, sha256); + "sha-384" -> + Caps#caps.version == make_disco_hash(Els, sha384); + "sha-512" -> + Caps#caps.version == make_disco_hash(Els, sha512); + _ -> + true + end. +-endif. concat_features(Els) -> lists:usort( @@ -417,3 +607,20 @@ concat_xdata_fields(Fields) -> Acc end, [<<>>, []], Fields), [Form, $<, lists:sort(Res)]. + +gb_trees_fold(F, Acc, Tree) -> + Iter = gb_trees:iterator(Tree), + gb_trees_fold_iter(F, Acc, Iter). + +gb_trees_fold_iter(F, Acc, Iter) -> + case gb_trees:next(Iter) of + {Key, Val, NewIter} -> + NewAcc = F(Key, Val, Acc), + gb_trees_fold_iter(F, NewAcc, NewIter); + _ -> + Acc + end. + +now_ts() -> + {MegaSecs, Secs, _} = now(), + MegaSecs*1000000 + Secs. diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl index 26912dd73..d16ce38ff 100644 --- a/src/mod_pubsub/mod_pubsub.erl +++ b/src/mod_pubsub/mod_pubsub.erl @@ -893,8 +893,8 @@ disco_items(#jid{raw = JID, node = U, domain = S, resource = R} = Host, NodeId, %% ------- %% presence hooks handling functions %% -caps_update(#jid{node = U, domain = S, resource = R} = From, To, _Features) -> - Pid = ejabberd_sm:get_session_pid(U, S, R), +caps_update(From, To, _Features) -> + Pid = ejabberd_sm:get_session_pid(From), presence_probe(From, To, Pid). -spec(presence_probe/3 :: @@ -2459,8 +2459,8 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> node_call(Type, publish_item, [Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload]) end end, - ServerHostB = list_to_binary(ServerHost), - ejabberd_hooks:run(pubsub_publish_item, ServerHostB, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), + %%ServerHostS = binary_to_list(ServerHost), + ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), Reply = #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'publish', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_PUBSUB, name = 'item', attrs = itemAttr(ItemId)}]}]},