Skip to content

Commit

Permalink
Merge pull request #4092 from esl/pubsub_update
Browse files Browse the repository at this point in the history
Updating XEP-0060 Publish-Subscribe

Check preconditions when accepting publish-options along with a publish request. We have two node types which advertise support for the publish-options feature.
In node push the preconditions are not checked. This node type is supporting xep-0163.
In node pep the changes are implemented, and the publish-options are being checked.
  • Loading branch information
NelsonVides authored Aug 15, 2023
2 parents f9dce47 + ea8e8ce commit e6b9cc2
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 22 deletions.
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 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access, Publish
{(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 @@ get_option(Options, Var, Def) ->
_ -> 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;
{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 @@ select_type(ServerHost, Host, Node, Type) ->
false -> hd(ConfiguredTypes)
end.

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

features() ->
Expand All @@ -4101,6 +4144,7 @@ features() ->
<<"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

0 comments on commit e6b9cc2

Please sign in to comment.