Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating XEP-0060 Publish-Subscribe #4092

Merged
merged 5 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 123 additions & 4 deletions big_tests/tests/pep_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
disco_sm_items_test/1,
pep_caps_test/1,
publish_and_notify_test/1,
publish_options_test/1,
auto_create_with_publish_options_test/1,
publish_options_success_test/1,
publish_options_fail_unknown_option_story/1,
publish_options_fail_wrong_value_story/1,
publish_options_fail_wrong_form/1,
send_caps_after_login_test/1,
delayed_receive/1,
delayed_receive_with_sm/1,
Expand All @@ -35,7 +39,8 @@

-export([
start_caps_clients/2,
send_initial_presence_with_caps/2
send_initial_presence_with_caps/2,
add_config_to_create_node_request/1
]).

-import(distributed_helper, [mim/0,
Expand All @@ -45,6 +50,8 @@
-import(config_parser_helper, [mod_config/2]).
-import(domain_helper, [domain/0]).

-define(NS_PUBSUB_PUB_OPTIONS, <<"http://jabber.org/protocol/pubsub#publish-options">>).

%%--------------------------------------------------------------------
%% Suite configuration
%%--------------------------------------------------------------------
Expand All @@ -64,7 +71,11 @@ groups() ->
disco_sm_items_test,
pep_caps_test,
publish_and_notify_test,
publish_options_test,
auto_create_with_publish_options_test,
publish_options_success_test,
publish_options_fail_unknown_option_story,
publish_options_fail_wrong_value_story,
publish_options_fail_wrong_form,
send_caps_after_login_test,
delayed_receive,
delayed_receive_with_sm,
Expand Down Expand Up @@ -206,7 +217,7 @@ publish_and_notify_story(Config, Alice, Bob) ->
pubsub_tools:receive_item_notification(
Bob, <<"item1">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []).

publish_options_test(Config) ->
auto_create_with_publish_options_test(Config) ->
% Given pubsub is configured with pep plugin
escalus:fresh_story(
Config,
Expand All @@ -223,6 +234,86 @@ publish_options_test(Config) ->
verify_publish_options(NodeConfig, PublishOptions)
end).

publish_options_success_test(Config) ->
escalus:fresh_story(
Config,
[{alice, 1}, {bob, 1}],
fun(Alice, Bob) ->
NodeNS = random_node_ns(),
PepNode = make_pep_node_info(Alice, NodeNS),
pubsub_tools:create_node(Alice, PepNode,
[{modify_request,fun add_config_to_create_node_request/1}]),
escalus_story:make_all_clients_friends([Alice, Bob]),
PublishOptions = [{<<"pubsub#deliver_payloads">>, <<"1">>},
{<<"pubsub#notify_config">>, <<"0">>},
{<<"pubsub#notify_delete">>, <<"0">>},
{<<"pubsub#purge_offline">>, <<"0">>},
{<<"pubsub#notify_retract">>, <<"0">>},
{<<"pubsub#persist_items">>, <<"1">>},
{<<"pubsub#roster_groups_allowed">>, [<<"friends">>, <<"enemies">>]},
{<<"pubsub#max_items">>, <<"1">>},
{<<"pubsub#subscribe">>, <<"1">>},
{<<"pubsub#access_model">>, <<"presence">>},
{<<"pubsub#publish_model">>, <<"publishers">>},
{<<"pubsub#notification_type">>, <<"headline">>},
{<<"pubsub#max_payload_size">>, <<"60000">>},
{<<"pubsub#send_last_published_item">>, <<"on_sub_and_presence">>},
{<<"pubsub#deliver_notifications">>, <<"1">>},
{<<"pubsub#presence_based_delivery">>, <<"1">>}],
Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
escalus:assert(is_iq_result, Result)
end).

publish_options_fail_unknown_option_story(Config) ->
escalus:fresh_story(
Config,
[{alice, 1}],
fun(Alice) ->
NodeNS = random_node_ns(),
PepNode = make_pep_node_info(Alice, NodeNS),
pubsub_tools:create_node(Alice, PepNode, []),

PublishOptions = [{<<"deliver_payloads">>, <<"1">>}],
Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result),

PublishOptions2 = [{<<"pubsub#not_existing_option">>, <<"1">>}],
Result2 = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions2),
escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result2)
end).

publish_options_fail_wrong_value_story(Config) ->
escalus:fresh_story(
Config,
[{alice, 1}],
fun(Alice) ->
NodeNS = random_node_ns(),
PepNode = make_pep_node_info(Alice, NodeNS),
pubsub_tools:create_node(Alice, PepNode,
[{modify_request,fun add_config_to_create_node_request/1}]),

PublishOptions = [{<<"pubsub#deliver_payloads">>, <<"0">>}],
Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result),

PublishOptions2 = [{<<"pubsub#roster_groups_allowed">>, <<"friends">>}],
Result2 = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions2),
escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result2)
end).

publish_options_fail_wrong_form(Config) ->
escalus:fresh_story(
Config,
[{alice, 1}],
fun(Alice) ->
NodeNS = random_node_ns(),
PepNode = make_pep_node_info(Alice, NodeNS),
pubsub_tools:create_node(Alice, PepNode, []),
PublishOptions = [{<<"deliver_payloads">>, <<"0">>}],
Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions, <<"WRONG_NS">>),
escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result)
end).

send_caps_after_login_test(Config) ->
escalus:fresh_story(
Config,
Expand Down Expand Up @@ -384,6 +475,34 @@ unsubscribe_after_presence_unsubscription(Config) ->
%% Helpers
%%-----------------------------------------------------------------

add_config_to_create_node_request(#xmlel{children = [PubsubEl]} = Request) ->
Fields = [#{values => [<<"friends">>, <<"enemies">>], var => <<"pubsub#roster_groups_allowed">>}],
Form = form_helper:form(#{ns => <<"http://jabber.org/protocol/pubsub#node_config">>, fields => Fields}),
ConfigureEl = #xmlel{name = <<"configure">>, children = [Form]},
PubsubEl2 = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [ConfigureEl]},
Request#xmlel{children = [PubsubEl2]}.

publish_with_publish_options(Client, Node, Content, Options) ->
publish_with_publish_options(Client, Node, Content, Options, ?NS_PUBSUB_PUB_OPTIONS).

publish_with_publish_options(Client, Node, Content, Options, FormType) ->
OptionsEl = #xmlel{name = <<"publish-options">>,
children = form(Options, FormType)},

Id = pubsub_tools:id(Client, Node, <<"publish">>),
Publish = pubsub_tools:publish_request(Id, Client, Content, Node, Options),
#xmlel{children = [#xmlel{} = PubsubEl]} = Publish,
NewPubsubEl = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [OptionsEl]},
escalus:send(Client, Publish#xmlel{children = [NewPubsubEl]}),
escalus:wait_for_stanza(Client).

form(FormFields, FormType) ->
FieldSpecs = lists:map(fun field_spec/1, FormFields),
[form_helper:form(#{fields => FieldSpecs, ns => FormType})].

field_spec({Var, Value}) when is_list(Value) -> #{var => Var, values => Value};
field_spec({Var, Value}) -> #{var => Var, values => [Value]}.

required_modules() ->
[{mod_caps, config_parser_helper:default_mod_config(mod_caps)},
{mod_pubsub, mod_config(mod_pubsub, #{plugins => [<<"dag">>, <<"pep">>],
Expand Down
8 changes: 7 additions & 1 deletion big_tests/tests/push_pubsub_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,14 @@ publish_fails_with_invalid_item(Config) ->
Item =
#xmlel{name = <<"invalid-item">>,
attrs = [{<<"xmlns">>, ?NS_PUSH}]},

Options = [
{<<"device_id">>, <<"sometoken">>},
{<<"service">>, <<"apns">>}
],

Publish = escalus_pubsub_stanza:publish(Alice, <<"itemid">>, Item, <<"id">>, Node),
Publish = escalus_pubsub_stanza:publish_with_options(Alice, <<"itemid">>, Item,
<<"id">>, Node, Options),
escalus:send(Alice, Publish),
escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
escalus:wait_for_stanza(Alice)),
Expand Down
2 changes: 1 addition & 1 deletion src/event_pusher/mod_event_pusher_push.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
-behavior(gen_mod).
-behavior(mod_event_pusher).
-behaviour(mongoose_module_metrics).
-xep([{xep, 357}, {version, "0.2.1"}]).
-xep([{xep, 357}, {version, "0.4.1"}]).

-include("mod_event_pusher_events.hrl").
-include("mongoose.hrl").
Expand Down
50 changes: 47 additions & 3 deletions src/pubsub/mod_pubsub.erl
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
-behaviour(mongoose_module_metrics).
-author('[email protected]').

-xep([{xep, 60}, {version, "1.13-1"}]).
-xep([{xep, 60}, {version, "1.25.0"}]).
-xep([{xep, 163}, {version, "1.2.2"}]).
-xep([{xep, 248}, {version, "0.3.0"}]).
-xep([{xep, 277}, {version, "0.6.5"}]).
Expand Down Expand Up @@ -2269,7 +2269,9 @@
{(DeliverPayloads == false) and (PersistItems == false) and (PayloadSize > 0),
extended_error(mongoose_xmpp_errors:bad_request(), <<"item-forbidden">>)},
{((DeliverPayloads == true) or (PersistItems == true)) and (PayloadSize == 0),
extended_error(mongoose_xmpp_errors:bad_request(), <<"item-required">>)}
extended_error(mongoose_xmpp_errors:bad_request(), <<"item-required">>)},
{PubOptsFeature andalso check_publish_options(Type, PublishOptions, Options),
extended_error(mongoose_xmpp_errors:conflict(), <<"precondition-not-met">>)}
],

case lists:keyfind(true, 1, Errors) of
Expand Down Expand Up @@ -3681,6 +3683,48 @@
_ -> Def
end.

-spec check_publish_options(binary(), undefined | exml:element(), mod_pubsub:nodeOptions()) ->
boolean().
check_publish_options(Type, PublishOptions, Options) ->
ParsedPublishOptions = parse_publish_options(PublishOptions),
ConvertedOptions = convert_options(Options),
case node_call(Type, check_publish_options, [ParsedPublishOptions, ConvertedOptions]) of
{error, _} ->
true;

Check warning on line 3693 in src/pubsub/mod_pubsub.erl

View check run for this annotation

Codecov / codecov/patch

src/pubsub/mod_pubsub.erl#L3693

Added line #L3693 was not covered by tests
{result, Result} ->
Result
end.

-spec parse_publish_options(undefined | exml:element()) -> invalid_form | #{binary() => [binary()]}.
parse_publish_options(undefined) ->
#{};
parse_publish_options(PublishOptions) ->
case mongoose_data_forms:find_and_parse_form(PublishOptions) of
#{type := <<"submit">>, kvs := KVs, ns := ?NS_PUBSUB_PUB_OPTIONS} ->
KVs;
_ ->
invalid_form
end.

-spec convert_options(mod_pubsub:nodeOptions()) -> #{binary() => [binary()]}.
convert_options(Options) ->
ConvertedOptions = lists:map(fun({Key, Value}) ->
{atom_to_binary(Key), convert_option_value(Value)}
end, Options),
maps:from_list(ConvertedOptions).

-spec convert_option_value(binary() | [binary()] | atom() | non_neg_integer()) -> [binary()].
convert_option_value(true) ->
[<<"1">>];
convert_option_value(false) ->
[<<"0">>];
convert_option_value(Element) when is_atom(Element) ->
[atom_to_binary(Element)];
convert_option_value(Element) when is_integer(Element) ->
[integer_to_binary(Element)];
convert_option_value(List) when is_list(List) ->
List.

node_options(Host, Type) ->
ConfiguredOpts = lists:keysort(1, config(serverhost(Host), default_node_config)),
DefaultOpts = lists:keysort(1, node_plugin_options(Type)),
Expand Down Expand Up @@ -4082,7 +4126,6 @@
false -> hd(ConfiguredTypes)
end.

feature(<<"rsm">>) -> ?NS_RSM;
feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>.

features() ->
Expand All @@ -4101,6 +4144,7 @@
<<"publisher-affiliation">>, % RECOMMENDED
<<"publish-only-affiliation">>, % OPTIONAL
<<"retrieve-default">>,
<<"rsm">>, % RECOMMENDED
<<"shim">>]. % RECOMMENDED
% see plugin "retrieve-items", % RECOMMENDED
% see plugin "retrieve-subscriptions", % RECOMMENDED
Expand Down
26 changes: 23 additions & 3 deletions src/pubsub/node_pep.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
unsubscribe_node/4, node_to_path/1,
get_entity_affiliations/2, get_entity_affiliations/3,
get_entity_subscriptions/2, get_entity_subscriptions/4,
should_delete_when_owner_removed/0
]).
should_delete_when_owner_removed/0, check_publish_options/2
]).

-ignore_xref([get_entity_affiliations/3, get_entity_subscriptions/4]).
-ignore_xref([get_entity_affiliations/3, get_entity_subscriptions/4, check_publish_options/2]).

based_on() -> node_flat.

Expand Down Expand Up @@ -92,6 +92,26 @@ features() ->
<<"retrieve-subscriptions">>,
<<"subscribe">>].

-spec check_publish_options(#{binary() => [binary()]} | invalid_form, #{binary() => [binary()]}) ->
boolean().
check_publish_options(invalid_form, _) ->
true;
check_publish_options(PublishOptions, NodeOptions) ->
F = fun(Key, Value) ->
case string:split(Key, "#") of
[<<"pubsub">>, Key2] ->
compare_values(Value, maps:get(Key2, NodeOptions, null));
_ -> true
end
end,
maps:size(maps:filter(F, PublishOptions)) =/= 0.

-spec compare_values([binary()], [binary()] | null) -> boolean().
compare_values(_, null) ->
true;
compare_values(Value1, Value2) ->
lists:sort(Value1) =/= lists:sort(Value2).

create_node_permission(Host, _ServerHost, _Node, _ParentNode,
#jid{ luser = <<>>, lserver = Host, lresource = <<>> }, _Access) ->
{result, true}; % pubsub service always allowed
Expand Down
25 changes: 15 additions & 10 deletions src/pubsub/node_push.erl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
-include("pubsub.hrl").

-export([based_on/0, init/3, terminate/2, options/0, features/0,
publish_item/9, node_to_path/1, should_delete_when_owner_removed/0]).
publish_item/9, node_to_path/1, should_delete_when_owner_removed/0,
check_publish_options/2]).

-ignore_xref([check_publish_options/2]).

based_on() -> node_flat.

Expand Down Expand Up @@ -72,17 +75,19 @@ publish_item(ServerHost, Nidx, Publisher, Model, _MaxItems, _ItemId, _ItemPublis
{error, mongoose_xmpp_errors:forbidden()}
end.

-spec check_publish_options(#{binary() => [binary()]} | invalid_form, #{binary() => [binary()]}) ->
boolean().
check_publish_options(#{<<"device_id">> := _, <<"service">> := _}, _) ->
false;
check_publish_options(_, _) ->
true.

do_publish_item(ServerHost, PublishOptions,
[#xmlel{name = <<"notification">>} | _] = Notifications) ->
case parse_form(PublishOptions) of
#{<<"device_id">> := _, <<"service">> := _} = OptionMap ->
NotificationForms = [parse_form(El) || El <- Notifications],
Result = mongoose_hooks:push_notifications(ServerHost, ok,
NotificationForms, OptionMap),
handle_push_hook_result(Result);
_ ->
{error, mod_pubsub:extended_error(mongoose_xmpp_errors:conflict(), <<"precondition-not-met">>)}
end;
NotificationForms = [parse_form(El) || El <- Notifications],
OptionMap = parse_form(PublishOptions),
Result = mongoose_hooks:push_notifications(ServerHost, ok, NotificationForms, OptionMap),
handle_push_hook_result(Result);
do_publish_item(_ServerHost, _PublishOptions, _Payload) ->
{error, mongoose_xmpp_errors:bad_request()}.

Expand Down