diff --git a/big_tests/tests/inbox_helper.erl b/big_tests/tests/inbox_helper.erl index 32582acedb5..f8963d032fc 100644 --- a/big_tests/tests/inbox_helper.erl +++ b/big_tests/tests/inbox_helper.erl @@ -163,8 +163,7 @@ insert_parallels(Gs) -> inbox_modules(Backend) -> [ - {mod_inbox, inbox_opts(Backend)}, - {mod_inbox_commands, #{}} + {mod_inbox, inbox_opts(Backend)} ]. muclight_modules() -> diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index c5f9b33b5e1..79c8c382d03 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -217,16 +217,10 @@ [modules.mod_disco] users_can_see_hidden_services = false -[modules.mod_commands] - {{#mod_cache_users}} [modules.mod_cache_users] {{{mod_cache_users}}} {{/mod_cache_users}} -[modules.mod_muc_commands] - -[modules.mod_muc_light_commands] - {{#mod_last}} [modules.mod_last] {{{mod_last}}} diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index c41cadbb63c..dfea351e799 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -51,7 +51,6 @@ start(normal, _Args) -> ejabberd_node_id:start(), ejabberd_ctl:init(), ejabberd_commands:init(), - mongoose_commands:init(), mongoose_graphql_commands:start(), mongoose_config:start(), mongoose_router:start(), diff --git a/src/ejabberd_cowboy.erl b/src/ejabberd_cowboy.erl index a390b52f77e..6f810362300 100644 --- a/src/ejabberd_cowboy.erl +++ b/src/ejabberd_cowboy.erl @@ -40,7 +40,7 @@ -export([ref/1, reload_dispatch/1]). -export([start_cowboy/4, start_cowboy/2, stop_cowboy/1]). --ignore_xref([behaviour_info/1, process/1, ref/1, socket_type/0, start_cowboy/2, +-ignore_xref([behaviour_info/1, process/1, ref/1, reload_dispatch/1, socket_type/0, start_cowboy/2, start_cowboy/4, start_link/1, start_listener/2, start_listener/1, stop_cowboy/1]). -include("mongoose.hrl"). diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index bff15a8e4d8..b87dec18f6f 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -165,7 +165,7 @@ process_inbox_boxes(Config = #{boxes := Boxes}) -> %% Cleaner gen_server callbacks start_cleaner(HostType, #{bin_ttl := TTL, bin_clean_after := Interval}) -> Name = gen_mod:get_module_proc(HostType, ?MODULE), - WOpts = #{host_type => HostType, action => fun mod_inbox_commands:flush_global_bin/2, + WOpts = #{host_type => HostType, action => fun mod_inbox_api:flush_global_bin/2, opts => TTL, interval => Interval}, MFA = {mongoose_collector, start_link, [Name, WOpts]}, ChildSpec = {Name, MFA, permanent, 5000, worker, [?MODULE]}, diff --git a/src/inbox/mod_inbox_commands.erl b/src/inbox/mod_inbox_commands.erl deleted file mode 100644 index 27b1e4022d2..00000000000 --- a/src/inbox/mod_inbox_commands.erl +++ /dev/null @@ -1,61 +0,0 @@ --module(mod_inbox_commands). - --behaviour(gen_mod). - -%% gen_mod --export([start/2, stop/1, supported_features/0]). - --export([flush_user_bin/3, flush_global_bin/2]). --ignore_xref([flush_user_bin/3, flush_global_bin/2]). - -%% Initialisation --spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%% Clean commands -commands() -> - [ - [{name, inbox_flush_user_bin}, - {category, <<"inbox">>}, - {subcategory, <<"bin">>}, - {desc, <<"Empty the bin for a user">>}, - {module, ?MODULE}, - {function, flush_user_bin}, - {action, delete}, - {identifiers, [domain, name, since]}, - {args, [{domain, binary}, - {name, binary}, - {since, integer}]}, - {result, {num, integer}}], - [{name, inbox_flush_global_bin}, - {category, <<"inbox">>}, - {subcategory, <<"bin">>}, - {desc, <<"Empty the inbox bin globally">>}, - {module, ?MODULE}, - {function, flush_global_bin}, - {action, delete}, - {identifiers, [host_type, since]}, - {args, [{host_type, binary}, - {since, integer}]}, - {result, {num, integer}}] - ]. - -flush_user_bin(Domain, Name, Days) -> - JID = jid:make_bare(Name, Domain), - Res = mod_inbox_api:flush_user_bin(JID, Days), - format_result(Res). - -flush_global_bin(HostType, Days) -> - Res = mod_inbox_api:flush_global_bin(HostType, Days), - format_result(Res). - -format_result({ok, Count}) -> Count; -format_result({_, ErrMsg}) -> {error, bad_request, ErrMsg}. diff --git a/src/mod_commands.erl b/src/mod_commands.erl deleted file mode 100644 index 6223a65a275..00000000000 --- a/src/mod_commands.erl +++ /dev/null @@ -1,457 +0,0 @@ --module(mod_commands). --author('bartlomiej.gorny@erlang-solutions.com'). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/0, stop/0, supported_features/0, - start/2, stop/1, - register/3, - unregister/2, - registered_commands/0, - registered_users/1, - change_user_password/3, - list_sessions/1, - list_contacts/1, - add_contact/2, - add_contact/3, - add_contact/4, - delete_contacts/2, - delete_contact/2, - subscription/3, - set_subscription/3, - kick_session/3, - get_recent_messages/3, - get_recent_messages/4, - send_message/3, - send_stanza/1 - ]). - --ignore_xref([add_contact/2, add_contact/3, add_contact/4, change_user_password/3, - delete_contact/2, delete_contacts/2, get_recent_messages/3, - get_recent_messages/4, kick_session/3, list_contacts/1, - list_sessions/1, register/3, registered_commands/0, registered_users/1, - send_message/3, send_stanza/1, set_subscription/3, start/0, stop/0, - subscription/3, unregister/2]). - --include("mongoose.hrl"). --include("jlib.hrl"). --include("mongoose_rsm.hrl"). --include("session.hrl"). - -start() -> - mongoose_commands:register(commands()). - -stop() -> - mongoose_commands:unregister(commands()). - -start(_, _) -> start(). -stop(_) -> stop(). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%%% -%%% mongoose commands -%%% - -commands() -> - [ - [ - {name, list_methods}, - {category, <<"commands">>}, - {desc, <<"List commands">>}, - {module, ?MODULE}, - {function, registered_commands}, - {action, read}, - {args, []}, - {result, []} - ], - [ - {name, list_users}, - {category, <<"users">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - [ - {name, register_user}, - {category, <<"users">>}, - {desc, <<"Register a user">>}, - {module, ?MODULE}, - {function, register}, - {action, create}, - {args, [{host, binary}, {username, binary}, {password, binary}]}, - {result, {msg, binary}} - ], - [ - {name, unregister_user}, - {category, <<"users">>}, - {desc, <<"UnRegister a user">>}, - {module, ?MODULE}, - {function, unregister}, - {action, delete}, - {args, [{host, binary}, {user, binary}]}, - {result, ok} - ], - [ - {name, list_sessions}, - {category, <<"sessions">>}, - {desc, <<"Get session list">>}, - {module, ?MODULE}, - {function, list_sessions}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - [ - {name, kick_user}, - {category, <<"sessions">>}, - {desc, <<"Terminate user connection">>}, - {module, ?MODULE}, - {function, kick_session}, - {action, delete}, - {args, [{host, binary}, {user, binary}, {res, binary}]}, - {result, ok} - ], - [ - {name, list_contacts}, - {category, <<"contacts">>}, - {desc, <<"Get roster">>}, - {module, ?MODULE}, - {function, list_contacts}, - {action, read}, - {args, [{caller, binary}]}, - {result, []} - ], - [ - {name, add_contact}, - {category, <<"contacts">>}, - {desc, <<"Add a contact to roster">>}, - {module, ?MODULE}, - {function, add_contact}, - {action, create}, - {args, [{caller, binary}, {jid, binary}]}, - {result, ok} - ], - [ - {name, subscription}, - {category, <<"contacts">>}, - {desc, <<"Send out a subscription request">>}, - {module, ?MODULE}, - {function, subscription}, - {action, update}, - {identifiers, [caller, jid]}, - % caller has to be in identifiers, otherwise it breaks admin rest api - {args, [{caller, binary}, {jid, binary}, {action, binary}]}, - {result, ok} - ], - [ - {name, set_subscription}, - {category, <<"contacts">>}, - {subcategory, <<"manage">>}, - {desc, <<"Set / unset mutual subscription">>}, - {module, ?MODULE}, - {function, set_subscription}, - {action, update}, - {identifiers, [caller, jid]}, - {args, [{caller, binary}, {jid, binary}, {action, binary}]}, - {result, ok} - ], - [ - {name, delete_contact}, - {category, <<"contacts">>}, - {desc, <<"Remove a contact from roster">>}, - {module, ?MODULE}, - {function, delete_contact}, - {action, delete}, - {args, [{caller, binary}, {jid, binary}]}, - {result, ok} - ], - [ - {name, delete_contacts}, - {category, <<"contacts">>}, - {subcategory, <<"multiple">>}, - {desc, <<"Remove provided contacts from roster">>}, - {module, ?MODULE}, - {function, delete_contacts}, - {action, delete}, - {args, [{caller, binary}, {jids, [binary]}]}, - {result, []} - ], - [ - {name, send_message}, - {category, <<"messages">>}, - {desc, <<"Send chat message from to">>}, - {module, ?MODULE}, - {function, send_message}, - {action, create}, - {args, [{caller, binary}, {to, binary}, {body, binary}]}, - {result, ok} - ], - [ - {name, send_stanza}, - {category, <<"stanzas">>}, - {desc, <<"Send an arbitrary stanza">>}, - {module, ?MODULE}, - {function, send_stanza}, - {action, create}, - {args, [{stanza, binary}]}, - {result, ok} - ], - [ - {name, get_last_messages_with_everybody}, - {category, <<"messages">>}, - {desc, <<"Get n last messages from archive, optionally before a certain date (unixtime)">>}, - {module, ?MODULE}, - {function, get_recent_messages}, - {action, read}, - {args, [{caller, binary}]}, - {optargs, [{before, integer, 0}, {limit, integer, 100}]}, - {result, []} - ], - [ - {name, get_last_messages}, - {category, <<"messages">>}, - {desc, <<"Get n last messages to/from given contact, with limit and date">>}, - {module, ?MODULE}, - {function, get_recent_messages}, - {action, read}, - {args, [{caller, binary}, {with, binary}]}, - {optargs, [{before, integer, 0}, {limit, integer, 100}]}, - {result, []} - ], - [ - {name, change_password}, - {category, <<"users">>}, - {desc, <<"Change user password">>}, - {module, ?MODULE}, - {function, change_user_password}, - {action, update}, - {identifiers, [host, user]}, - {args, [{host, binary}, {user, binary}, {newpass, binary}]}, - {result, ok} - ] - ]. - -kick_session(Host, User, Resource) -> - case mongoose_session_api:kick_session(User, Host, Resource, <<"kicked">>) of - {ok, Msg} -> Msg; - {no_session, Msg} -> {error, not_found, Msg} - end. - -list_sessions(Host) -> - mongoose_session_api:list_resources(Host). - -registered_users(Host) -> - mongoose_account_api:list_users(Host). - -register(Host, User, Password) -> - Res = mongoose_account_api:register_user(User, Host, Password), - format_account_result(Res). - -unregister(Host, User) -> - Res = mongoose_account_api:unregister_user(User, Host), - format_account_result(Res). - -change_user_password(Host, User, Password) -> - Res = mongoose_account_api:change_password(User, Host, Password), - format_account_result(Res). - -format_account_result({ok, Msg}) -> iolist_to_binary(Msg); -format_account_result({empty_password, Msg}) -> {error, bad_request, Msg}; -format_account_result({invalid_jid, Msg}) -> {error, bad_request, Msg}; -format_account_result({not_allowed, Msg}) -> {error, denied, Msg}; -format_account_result({exists, Msg}) -> {error, denied, Msg}; -format_account_result({cannot_register, Msg}) -> {error, internal, Msg}. - -send_message(From, To, Body) -> - case mongoose_stanza_helper:parse_from_to(From, To) of - {ok, FromJID, ToJID} -> - Packet = mongoose_stanza_helper:build_message(From, To, Body), - do_send_packet(FromJID, ToJID, Packet); - Error -> - Error - end. - -send_stanza(BinStanza) -> - case exml:parse(BinStanza) of - {ok, Packet} -> - From = exml_query:attr(Packet, <<"from">>), - To = exml_query:attr(Packet, <<"to">>), - case mongoose_stanza_helper:parse_from_to(From, To) of - {ok, FromJID, ToJID} -> - do_send_packet(FromJID, ToJID, Packet); - {error, missing} -> - {error, bad_request, "both from and to are required"}; - {error, type_error, E} -> - {error, type_error, E} - end; - {error, Reason} -> - {error, bad_request, io_lib:format("Malformed stanza: ~p", [Reason])} - end. - -do_send_packet(From, To, Packet) -> - case mongoose_domain_api:get_domain_host_type(From#jid.lserver) of - {ok, HostType} -> - Acc = mongoose_acc:new(#{location => ?LOCATION, - host_type => HostType, - lserver => From#jid.lserver, - element => Packet}), - Acc1 = mongoose_hooks:user_send_packet(Acc, From, To, Packet), - ejabberd_router:route(From, To, Acc1), - ok; - {error, not_found} -> - {error, unknown_domain} - end. - -list_contacts(Caller) -> - case mod_roster_api:list_contacts(jid:from_binary(Caller)) of - {ok, Rosters} -> - [roster_info(mod_roster:item_to_map(R)) || R <- Rosters]; - Error -> - skip_result_msg(Error) - end. - -roster_info(M) -> - Jid = jid:to_binary(maps:get(jid, M)), - #{subscription := Sub, ask := Ask} = M, - #{jid => Jid, subscription => Sub, ask => Ask}. - -add_contact(Caller, JabberID) -> - add_contact(Caller, JabberID, <<"">>, []). - -add_contact(Caller, JabberID, Name) -> - add_contact(Caller, JabberID, Name, []). - -add_contact(Caller, Other, Name, Groups) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJid, OtherJid} -> - Res = mod_roster_api:add_contact(CallerJid, OtherJid, Name, Groups), - skip_result_msg(Res); - E -> - E - end. - -delete_contacts(Caller, ToDelete) -> - maybe_delete_contacts(Caller, ToDelete, []). - -maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted; -maybe_delete_contacts(Caller, [H | T], NotDeleted) -> - case delete_contact(Caller, H) of - ok -> - maybe_delete_contacts(Caller, T, NotDeleted); - _Error -> - maybe_delete_contacts(Caller, T, NotDeleted ++ [H]) - end. - -delete_contact(Caller, Other) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - Res = mod_roster_api:delete_contact(CallerJID, OtherJID), - skip_result_msg(Res); - E -> - E - end. - -registered_commands() -> - Items = collect_commands(), - sort_commands(Items). - -sort_commands(Items) -> - WithKey = [{get_sorting_key(Item), Item} || Item <- Items], - Sorted = lists:keysort(1, WithKey), - [Item || {_Key, Item} <- Sorted]. - -get_sorting_key(Item) -> - maps:get(path, Item). - -collect_commands() -> - [#{name => mongoose_commands:name(C), - category => mongoose_commands:category(C), - action => mongoose_commands:action(C), - method => mongoose_api_common:action_to_method(mongoose_commands:action(C)), - desc => mongoose_commands:desc(C), - args => format_args(mongoose_commands:args(C)), - path => mongoose_api_common:create_admin_url_path(C) - } || C <- mongoose_commands:list(admin)]. - -format_args(Args) -> - maps:from_list([{term_as_binary(Name), term_as_binary(rewrite_type(Type))} - || {Name, Type} <- Args]). - -%% binary is useful internally, but could confuse a regular user -rewrite_type(binary) -> string; -rewrite_type(Type) -> Type. - -term_as_binary(X) -> - iolist_to_binary(io_lib:format("~p", [X])). - -get_recent_messages(Caller, Before, Limit) -> - get_recent_messages(Caller, undefined, Before, Limit). - -get_recent_messages(Caller, With, Before, Limit) -> - Before2 = maybe_seconds_to_microseconds(Before), - Res = mongoose_stanza_api:lookup_recent_messages(Caller, With, Before2, Limit), - lists:map(fun row_to_map/1, Res). - -maybe_seconds_to_microseconds(X) when is_number(X) -> - X * 1000000; -maybe_seconds_to_microseconds(X) -> - X. - --spec row_to_map(mod_mam:message_row()) -> map(). -row_to_map(#{id := Id, jid := From, packet := Msg}) -> - Jbin = jid:to_binary(From), - {Msec, _} = mod_mam_utils:decode_compact_uuid(Id), - MsgId = case xml:get_tag_attr(<<"id">>, Msg) of - {value, MId} -> MId; - false -> <<"">> - end, - Body = exml_query:path(Msg, [{element, <<"body">>}, cdata]), - #{sender => Jbin, timestamp => round(Msec / 1000000), message_id => MsgId, - body => Body}. - -subscription(Caller, Other, Action) -> - case decode_action(Action) of - error -> - {error, bad_request, <<"invalid action">>}; - Act -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - Res = mod_roster_api:subscription(CallerJID, OtherJID, Act), - skip_result_msg(Res); - E -> - E - end - end. - -decode_action(<<"subscribe">>) -> subscribe; -decode_action(<<"subscribed">>) -> subscribed; -decode_action(_) -> error. - -set_subscription(Caller, Other, Action) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - case decode_both_sub_action(Action) of - error -> - {error, bad_request, <<"invalid action">>}; - ActionDecoded -> - Res = mod_roster_api:set_mutual_subscription(CallerJID, OtherJID, - ActionDecoded), - skip_result_msg(Res) - end; - E -> - E - end. - -decode_both_sub_action(<<"connect">>) -> connect; -decode_both_sub_action(<<"disconnect">>) -> disconnect; -decode_both_sub_action(_) -> error. - -skip_result_msg({ok, _Msg}) -> ok; -skip_result_msg({ErrCode, _Msg}) -> {error, ErrCode}. diff --git a/src/mod_muc_commands.erl b/src/mod_muc_commands.erl deleted file mode 100644 index acc299fdca2..00000000000 --- a/src/mod_muc_commands.erl +++ /dev/null @@ -1,174 +0,0 @@ -%%============================================================================== -%% Copyright 2016 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% Author: Joseph Yiasemides -%% Description: Administration commands for Mult-user Chat (MUC) -%%============================================================================== - --module(mod_muc_commands). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/2, stop/1, supported_features/0]). - --export([create_instant_room/4]). --export([invite_to_room/5]). --export([send_message_to_room/4]). --export([kick_user_from_room/3]). - --include_lib("jid/include/jid.hrl"). - --ignore_xref([create_instant_room/4, invite_to_room/5, kick_user_from_room/3, - send_message_to_room/4]). - -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -commands() -> - [ - [{name, create_muc_room}, - {category, <<"mucs">>}, - {desc, <<"Create a MUC room.">>}, - {module, ?MODULE}, - {function, create_instant_room}, - {action, create}, - {identifiers, [domain]}, - {args, - %% The argument `domain' is what we normally term the XMPP - %% domain, `name' is the room name, `owner' is the XMPP entity - %% that would normally request an instant MUC room. - [{domain, binary}, - {name, binary}, - {owner, binary}, - {nick, binary}]}, - {result, {name, binary}}], - - [{name, invite_to_muc_room}, - {category, <<"mucs">>}, - {subcategory, <<"participants">>}, - {desc, <<"Send a MUC room invite from one user to another.">>}, - {module, ?MODULE}, - {function, invite_to_room}, - {action, create}, - {identifiers, [domain, name]}, - {args, - [{domain, binary}, - {name, binary}, - {sender, binary}, - {recipient, binary}, - {reason, binary} - ]}, - {result, ok}], - - [{name, send_message_to_room}, - {category, <<"mucs">>}, - {subcategory, <<"messages">>}, - {desc, <<"Send a message to a MUC room from a given user.">>}, - {module, ?MODULE}, - {function, send_message_to_room}, - {action, create}, - {identifiers, [domain, name]}, - {args, - [{domain, binary}, - {name, binary}, - {from, binary}, - {body, binary} - ]}, - {result, ok}], - - [{name, kick_user_from_room}, - {category, <<"mucs">>}, - {desc, - <<"Kick a user from a MUC room (on behalf of a moderator).">>}, - {module, ?MODULE}, - {function, kick_user_from_room}, - {action, delete}, - {identifiers, [domain, name, nick]}, - {args, - [{domain, binary}, - {name, binary}, - {nick, binary} - ]}, - {result, ok}] - - ]. - -create_instant_room(Domain, Name, Owner, Nick) -> - case jid:binary_to_bare(Owner) of - error -> - error; - OwnerJID -> - #jid{luser = RName, lserver = MUCServer} = room_jid(Domain, Name), - case mod_muc_api:create_instant_room(MUCServer, RName, OwnerJID, Nick) of - {ok, #{title := RName}} -> RName; - Error -> make_rest_error(Error) - end - end. - -invite_to_room(Domain, Name, Sender, Recipient, Reason) -> - case mongoose_stanza_helper:parse_from_to(Sender, Recipient) of - {ok, SenderJID, RecipientJID} -> - RoomJID = room_jid(Domain, Name), - case mod_muc_api:invite_to_room(RoomJID, SenderJID, RecipientJID, Reason) of - {ok, _} -> - ok; - Error -> - make_rest_error(Error) - end; - Error -> - Error - end. - -send_message_to_room(Domain, Name, Sender, Message) -> - RoomJID = room_jid(Domain, Name), - case jid:from_binary(Sender) of - error -> - error; - SenderJID -> - mod_muc_api:send_message_to_room(RoomJID, SenderJID, Message) - end. - -kick_user_from_room(Domain, Name, Nick) -> - Reason = <<"User kicked from the admin REST API">>, - RoomJID = room_jid(Domain, Name), - case mod_muc_api:kick_user_from_room(RoomJID, Nick, Reason) of - {ok, _} -> - ok; - Error -> - make_rest_error(Error) - end. - -%%-------------------------------------------------------------------- -%% Ancillary -%%-------------------------------------------------------------------- - --spec room_jid(jid:lserver(), binary()) -> jid:jid() | error. -room_jid(Domain, Name) -> - {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), - MUCDomain = mod_muc:server_host_to_muc_host(HostType, Domain), - jid:make(Name, MUCDomain, <<>>). - -make_rest_error({room_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({user_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({moderator_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({internal, ErrMsg}) -> {error, internal, ErrMsg}. diff --git a/src/mongoose_api_admin.erl b/src/mongoose_api_admin.erl deleted file mode 100644 index 0419a3d8bf3..00000000000 --- a/src/mongoose_api_admin.erl +++ /dev/null @@ -1,218 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author ludwikbukowski -%%% @copyright (C) 2016, Erlang Solutions Ltd. -%%% Created : 05. Jul 2016 12:59 -%%%------------------------------------------------------------------- - -%% @doc MongooseIM REST HTTP API for administration. -%% This module implements cowboy REST callbacks and -%% passes the requests on to the http api backend module. -%% @end --module(mongoose_api_admin). --author("ludwikbukowski"). - --behaviour(mongoose_http_handler). --behaviour(cowboy_rest). - -%% mongoose_http_handler callbacks --export([config_spec/0, routes/1]). - -%% config processing callbacks --export([process_config/1]). - -%% cowboy_rest exports --export([allowed_methods/2, - content_types_provided/2, - terminate/3, - init/2, - options/2, - content_types_accepted/2, - delete_resource/2, - is_authorized/2]). -%% local callbacks --export([to_json/2, from_json/2]). - --ignore_xref([cowboy_router_paths/2, from_json/2, to_json/2]). - --include("mongoose_api.hrl"). --include("mongoose.hrl"). --include("mongoose_config_spec.hrl"). - --import(mongoose_api_common, [error_response/4, - action_to_method/1, - method_to_action/1, - error_code/1, - process_request/4, - parse_request_body/1]). - --type credentials() :: {Username :: binary(), Password :: binary()} | any. - --type handler_options() :: #{path := string(), username => binary(), password => binary(), - atom() => any()}. - -%%-------------------------------------------------------------------- -%% mongoose_http_handler callbacks -%%-------------------------------------------------------------------- - --spec config_spec() -> mongoose_config_spec:config_section(). -config_spec() -> - #section{items = #{<<"username">> => #option{type = binary}, - <<"password">> => #option{type = binary}}, - process = fun ?MODULE:process_config/1}. - --spec process_config(handler_options()) -> handler_options(). -process_config(Opts) -> - case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of - true -> - Opts; - false -> - error(#{what => both_username_and_password_required, opts => Opts}) - end. - --spec routes(handler_options()) -> mongoose_http_handler:routes(). -routes(Opts = #{path := BasePath}) -> - ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), - ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), - try - Commands = mongoose_commands:list(admin), - [handler_path(BasePath, Command, Opts) || Command <- Commands] - catch - Class:Err:StackTrace -> - ?LOG_ERROR(#{what => getting_command_list_error, - class => Class, reason => Err, stacktrace => StackTrace}), - [] - end. - -%%-------------------------------------------------------------------- -%% cowboy_rest callbacks -%%-------------------------------------------------------------------- - -init(Req, Opts) -> - Bindings = maps:to_list(cowboy_req:bindings(Req)), - #{command_category := CommandCategory, command_subcategory := CommandSubCategory} = Opts, - Auth = auth_opts(Opts), - State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(admin), - bindings = Bindings, - command_category = CommandCategory, - command_subcategory = CommandSubCategory, - auth = Auth}, - {cowboy_rest, Req, State}. - -auth_opts(#{username := UserName, password := Password}) -> {UserName, Password}; -auth_opts(#{}) -> any. - -options(Req, State) -> - Req1 = set_cors_headers(Req), - {ok, Req1, State}. - -set_cors_headers(Req) -> - Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>, - <<"GET, OPTIONS, PUT, POST, DELETE">>, Req), - Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, - <<"*">>, Req1), - cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>, - <<"Content-Type">>, Req2). - -allowed_methods(Req, #http_api_state{command_category = Name} = State) -> - CommandList = mongoose_commands:list(admin, Name), - AllowedMethods = [action_to_method(mongoose_commands:action(Command)) - || Command <- CommandList], - {[<<"OPTIONS">> | AllowedMethods], Req, State}. - -content_types_provided(Req, State) -> - CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], - {CTP, Req, State}. - -content_types_accepted(Req, State) -> - CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], - {CTA, Req, State}. - -terminate(_Reason, _Req, _State) -> - ok. - -%% @doc Called for a method of type "DELETE" -delete_resource(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Arity = length(B), - Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"DELETE">>), SubCategory), - [Command] = [C || C <- Cmds, mongoose_commands:arity(C) == Arity], - process_request(<<"DELETE">>, Command, Req, State). - - -%%-------------------------------------------------------------------- -%% Authorization -%%-------------------------------------------------------------------- - -% @doc Cowboy callback -is_authorized(Req, State) -> - ControlCreds = get_control_creds(State), - AuthDetails = mongoose_api_common:get_auth_details(Req), - case authorize(ControlCreds, AuthDetails) of - true -> - {true, Req, State}; - false -> - mongoose_api_common:make_unauthorized_response(Req, State) - end. - --spec authorize(credentials(), {AuthMethod :: atom(), - Username :: binary(), - Password :: binary()}) -> boolean(). -authorize(any, _) -> true; -authorize(_, undefined) -> false; -authorize(ControlCreds, {AuthMethod, User, Password}) -> - compare_creds(ControlCreds, {User, Password}) andalso - mongoose_api_common:is_known_auth_method(AuthMethod). - -% @doc Checks if credentials are the same (if control creds are 'any' -% it is equal to everything). --spec compare_creds(credentials(), credentials() | undefined) -> boolean(). -compare_creds({User, Pass}, {User, Pass}) -> true; -compare_creds(_, _) -> false. - -get_control_creds(#http_api_state{auth = Creds}) -> - Creds. - -%%-------------------------------------------------------------------- -%% Internal funs -%%-------------------------------------------------------------------- - -%% @doc Called for a method of type "GET" -to_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"GET">>), SubCategory), - Arity = length(B), - case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of - [Command] -> - process_request(<<"GET">>, Command, Req, State); - [] -> - error_response(not_found, ?ARGS_LEN_ERROR, Req, State) - end. - -%% @doc Called for a method of type "POST" and "PUT" -from_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - case parse_request_body(Req) of - {error, _R}-> - error_response(bad_request, ?BODY_MALFORMED, Req, State); - {Params, _} -> - Method = cowboy_req:method(Req), - Cmds = mongoose_commands:list(admin, Category, method_to_action(Method), SubCategory), - QVals = cowboy_req:parse_qs(Req), - Arity = length(B) + length(Params) + length(QVals), - case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of - [Command] -> - process_request(Method, Command, {Params, Req}, State); - [] -> - error_response(not_found, ?ARGS_LEN_ERROR, Req, State) - end - end. - --spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t(), handler_options()) -> - ejabberd_cowboy:route(). -handler_path(Base, Command, CommonOpts) -> - {[Base, mongoose_api_common:create_admin_url_path(Command)], - ?MODULE, CommonOpts#{command_category => mongoose_commands:category(Command), - command_subcategory => mongoose_commands:subcategory(Command)}}. diff --git a/src/mongoose_api_common.erl b/src/mongoose_api_common.erl index 7bb1fc39fe8..017cd292313 100644 --- a/src/mongoose_api_common.erl +++ b/src/mongoose_api_common.erl @@ -3,61 +3,7 @@ %%% @copyright (C) 2016, Erlang Solutions Ltd. %%% Created : 20. Jul 2016 10:16 %%%------------------------------------------------------------------- - -%% @doc MongooseIM REST API backend -%% -%% This module handles the client HTTP REST requests, then respectively convert -%% them to Commands from mongoose_commands and execute with `admin` privileges. -%% It supports responses with appropriate HTTP Status codes returned to the -%% client. -%% This module implements behaviour introduced in ejabberd_cowboy which is -%% %% built on top of the cowboy library. -%% The method supported: GET, POST, PUT, DELETE. Only JSON format. -%% The library "jiffy" used to serialize and deserialized JSON data. -%% -%% REQUESTS -%% -%% The module is based on mongoose_commands registry. -%% The root http path for a command is build based on the "category" field. -%% %% It's always used as path a prefix. -%% The commands are translated to HTTP API in the following manner: -%% -%% command of action "read" will be called by GET request -%% command of action "create" will be called by POST request -%% command of action "update" will be called by PUT request -%% command of action "delete" will be called by DELETE request -%% -%% The args of the command will be filled with the values provided in path -%% %% bindings or body parameters, depending of the method type: -%% - for command of action "read" or "delete" all the args are pulled from the -%% path bindings. The path should be constructed of pairs "/arg_name/arg_value" -%% so that it could match the {arg_name, type} %% pattern in the command -%% registry. E.g having the record of category "users" and args: -%% [{username, binary}, {domain, binary}] we will have to make following GET -%% request %% path: http://domain:port/api/users/username/Joe/domain/localhost -%% and the command will be called with arguments "Joe" and "localhost" -%% -%% - for command of action "create" or "update" args are pulled from the body -%% JSON, except those that are on the "identifiers" list of the command. Those -%% go to the path bindings. -%% E.g having the record of category "animals", action "update" and args: -%% [{species, binary}, {name, binary}, {age, integer}] -%% and identifiers: -%% [species, name] -%% we can set the age for our elephant Ed in the PUT request: -%% path: http://domain:port/api/species/elephant/name/Ed -%% body: {"age" : "10"} -%% and then the command will be called with arguments ["elephant", "Ed" and 10]. -%% -%% RESPONSES -%% -%% The API supports some of the http status code like 200, 201, 400, 404 etc -%% depending on the return value of the command execution and arguments checks. -%% Additionally, when the command's action is "create" and it returns a value, -%% it is concatenated to the path and return to the client in header "location" -%% with response code 201 so that it could represent now a new created resource. -%% If error occured while executing the command, the appropriate reason is -%% returned in response body. +%%% @doc Utilities for the REST API -module(mongoose_api_common). -author("ludwikbukowski"). @@ -65,119 +11,13 @@ -include("mongoose.hrl"). %% API --export([create_admin_url_path/1, - action_to_method/1, - method_to_action/1, - parse_request_body/1, - get_allowed_methods/1, - process_request/4, - reload_dispatches/1, +-export([parse_request_body/1, get_auth_details/1, is_known_auth_method/1, error_response/4, make_unauthorized_response/2, check_password/2]). --ignore_xref([reload_dispatches/1]). - -%% @doc Reload all ejabberd_cowboy listeners. -%% When a command is registered or unregistered, the routing paths that -%% cowboy stores as a "dispatch" must be refreshed. -%% Read more http://ninenines.eu/docs/en/cowboy/1.0/guide/routing/ -reload_dispatches(drop) -> - drop; -reload_dispatches(_Command) -> - Listeners = supervisor:which_children(mongoose_listener_sup), - CowboyListeners = [Child || {_Id, Child, _Type, [ejabberd_cowboy]} <- Listeners], - [ejabberd_cowboy:reload_dispatch(Child) || Child <- CowboyListeners], - drop. - - --spec create_admin_url_path(mongoose_commands:t()) -> ejabberd_cowboy:path(). -create_admin_url_path(Command) -> - iolist_to_binary(create_admin_url_path_iodata(Command)). - -create_admin_url_path_iodata(Command) -> - ["/", mongoose_commands:category(Command), - maybe_add_bindings(Command, admin), maybe_add_subcategory(Command)]. - --spec process_request(Method :: method(), - Command :: mongoose_commands:t(), - Req :: cowboy_req:req() | {list(), cowboy_req:req()}, - State :: http_api_state()) -> - {any(), cowboy_req:req(), http_api_state()}. -process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity} = State) - when ((Method == <<"POST">>) or (Method == <<"PUT">>)) -> - {Params, Req2} = Req, - QVals = cowboy_req:parse_qs(Req2), - QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals], - Params2 = Binds ++ Params ++ QV ++ maybe_add_caller(Entity), - handle_request(Method, Command, Params2, Req2, State); -process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity}=State) - when ((Method == <<"GET">>) or (Method == <<"DELETE">>)) -> - QVals = cowboy_req:parse_qs(Req), - QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals], - BindsAndVars = Binds ++ QV ++ maybe_add_caller(Entity), - handle_request(Method, Command, BindsAndVars, Req, State). - --spec handle_request(Method :: method(), - Command :: mongoose_commands:t(), - Args :: args_applied(), - Req :: cowboy_req:req(), - State :: http_api_state()) -> - {any(), cowboy_req:req(), http_api_state()}. -handle_request(Method, Command, Args, Req, #http_api_state{entity = Entity} = State) -> - case check_and_extract_args(mongoose_commands:args(Command), - mongoose_commands:optargs(Command), Args) of - {error, Type, Reason} -> - handle_result(Method, {error, Type, Reason}, Req, State); - ConvertedArgs -> - handle_result(Method, - execute_command(ConvertedArgs, Command, Entity), - Req, State) - end. - --type correct_result() :: mongoose_commands:success(). --type error_result() :: mongoose_commands:failure(). - --spec handle_result(Method, Result, Req, State) -> Return when - Method :: method() | no_call, - Result :: correct_result() | error_result(), - Req :: cowboy_req:req(), - State :: http_api_state(), - Return :: {any(), cowboy_req:req(), http_api_state()}. -handle_result(<<"DELETE">>, ok, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"DELETE">>, {ok, Res}, Req, State) -> - Req2 = cowboy_req:set_resp_body(jiffy:encode(Res), Req), - Req3 = cowboy_req:reply(200, Req2), - {jiffy:encode(Res), Req3, State}; -handle_result(Verb, ok, Req, State) -> - handle_result(Verb, {ok, nocontent}, Req, State); -handle_result(<<"GET">>, {ok, Result}, Req, State) -> - {jiffy:encode(Result), Req, State}; -handle_result(<<"POST">>, {ok, nocontent}, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"POST">>, {ok, Res}, Req, State) -> - Path = iolist_to_binary(cowboy_req:uri(Req)), - Req2 = cowboy_req:set_resp_body(Res, Req), - Req3 = maybe_add_location_header(Res, binary_to_list(Path), Req2), - {stop, Req3, State}; -handle_result(<<"PUT">>, {ok, nocontent}, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"PUT">>, {ok, Res}, Req, State) -> - Req2 = cowboy_req:set_resp_body(Res, Req), - Req3 = cowboy_req:reply(201, Req2), - {stop, Req3, State}; -handle_result(_, {error, Error, Reason}, Req, State) -> - error_response(Error, Reason, Req, State); -handle_result(no_call, _, Req, State) -> - error_response(not_implemented, <<>>, Req, State). - - -spec parse_request_body(any()) -> {args_applied(), cowboy_req:req()} | {error, any()}. parse_request_body(Req) -> {ok, Body, Req2} = cowboy_req:read_body(Req), @@ -192,120 +32,10 @@ parse_request_body(Req) -> {error, Reason} end. -%% @doc Checks if the arguments are correct. Return the arguments that can be applied to the -%% execution of command. --spec check_and_extract_args(arg_spec_list(), optarg_spec_list(), args_applied()) -> - map() | {error, atom(), any()}. -check_and_extract_args(ReqArgs, OptArgs, RequestArgList) -> - try - AllArgs = ReqArgs ++ [{N, T} || {N, T, _} <- OptArgs], - AllArgVals = [{N, T, proplists:get_value(N, RequestArgList)} || {N, T} <- AllArgs], - ConvArgs = [{N, convert_arg(T, V)} || {N, T, V} <- AllArgVals, V =/= undefined], - maps:from_list(ConvArgs) - catch - Class:Reason:StackTrace -> - ?LOG_ERROR(#{what => check_and_extract_args_failed, class => Class, - reason => Reason, stacktrace => StackTrace}), - {error, bad_request, Reason} - end. - --spec execute_command(mongoose_commands:args(), - mongoose_commands:t(), - mongoose_commands:caller()) -> - correct_result() | error_result(). -execute_command(ArgMap, Command, Entity) -> - mongoose_commands:execute(Entity, mongoose_commands:name(Command), ArgMap). - --spec maybe_add_caller(admin | binary()) -> list() | list({caller, binary()}). -maybe_add_caller(admin) -> - []; -maybe_add_caller(JID) -> - [{caller, JID}]. - --spec maybe_add_location_header(binary() | list(), list(), any()) - -> cowboy_req:req(). -maybe_add_location_header(Result, ResourcePath, Req) when is_binary(Result) -> - add_location_header(binary_to_list(Result), ResourcePath, Req); -maybe_add_location_header(Result, ResourcePath, Req) when is_list(Result) -> - add_location_header(Result, ResourcePath, Req); -maybe_add_location_header(_, _Path, Req) -> - cowboy_req:reply(204, #{}, Req). - -add_location_header(Result, ResourcePath, Req) -> - Path = [ResourcePath, "/", Result], - Headers = #{<<"location">> => Path}, - cowboy_req:reply(201, Headers, Req). - --spec convert_arg(atom(), any()) -> boolean() | integer() | float() | binary() | string() | {error, bad_type}. -convert_arg(binary, Binary) when is_binary(Binary) -> - Binary; -convert_arg(boolean, Value) when is_boolean(Value) -> - Value; -convert_arg(integer, Binary) when is_binary(Binary) -> - binary_to_integer(Binary); -convert_arg(integer, Integer) when is_integer(Integer) -> - Integer; -convert_arg(float, Binary) when is_binary(Binary) -> - binary_to_float(Binary); -convert_arg(float, Float) when is_float(Float) -> - Float; -convert_arg([Type], List) when is_list(List) -> - [ convert_arg(Type, Item) || Item <- List ]; -convert_arg(_, _Binary) -> - throw({error, bad_type}). - -spec create_params_proplist(list({binary(), binary()})) -> args_applied(). create_params_proplist(ArgList) -> lists:sort([{to_atom(Arg), Value} || {Arg, Value} <- ArgList]). -%% @doc Returns list of allowed methods. --spec get_allowed_methods(admin | user) -> list(method()). -get_allowed_methods(Entity) -> - Commands = mongoose_commands:list(Entity), - [action_to_method(mongoose_commands:action(Command)) || Command <- Commands]. - --spec maybe_add_bindings(mongoose_commands:t(), admin|user) -> iolist(). -maybe_add_bindings(Command, Entity) -> - Action = mongoose_commands:action(Command), - Args = mongoose_commands:args(Command), - BindAndBody = both_bind_and_body(Action), - case BindAndBody of - true -> - Ids = mongoose_commands:identifiers(Command), - Bindings = [El || {Key, _Value} = El <- Args, true =:= proplists:is_defined(Key, Ids)], - add_bindings(Bindings, Entity); - false -> - add_bindings(Args, Entity) - end. - -maybe_add_subcategory(Command) -> - SubCategory = mongoose_commands:subcategory(Command), - case SubCategory of - undefined -> - []; - _ -> - ["/", SubCategory] - end. - --spec both_bind_and_body(mongoose_commands:action()) -> boolean(). -both_bind_and_body(update) -> - true; -both_bind_and_body(create) -> - true; -both_bind_and_body(read) -> - false; -both_bind_and_body(delete) -> - false. - -add_bindings(Args, Entity) -> - [add_bind(A, Entity) || A <- Args]. - -%% skip "caller" arg for frontend command -add_bind({ArgName, _}, _Entity) -> - lists:flatten(["/:", atom_to_list(ArgName)]); -add_bind(Other, _) -> - throw({error, bad_arg_spec, Other}). - -spec to_atom(binary() | atom()) -> atom(). to_atom(Bin) when is_binary(Bin) -> erlang:binary_to_existing_atom(Bin, utf8); @@ -315,21 +45,13 @@ to_atom(Atom) when is_atom(Atom) -> %%-------------------------------------------------------------------- %% HTTP utils %%-------------------------------------------------------------------- -%%-spec error_response(mongoose_commands:errortype(), any(), http_api_state()) -> -%% {stop, any(), http_api_state()}. -%%error_response(ErrorType, Req, State) -> -%% error_response(ErrorType, <<>>, Req, State). --spec error_response(mongoose_commands:errortype(), mongoose_commands:errorreason(), cowboy_req:req(), http_api_state()) -> +-spec error_response(atom(), iodata(), cowboy_req:req(), http_api_state()) -> {stop, cowboy_req:req(), http_api_state()}. error_response(ErrorType, Reason, Req, State) -> - BinReason = case Reason of - B when is_binary(B) -> B; - L when is_list(L) -> list_to_binary(L); - Other -> list_to_binary(io_lib:format("~p", [Other])) - end, + BinReason = iolist_to_binary(Reason), ?LOG_ERROR(#{what => rest_common_error, - error_type => ErrorType, reason => Reason, req => Req}), + error_type => ErrorType, reason => BinReason, req => Req}), Req1 = cowboy_req:reply(error_code(ErrorType), #{}, BinReason, Req), {stop, Req1, State}. @@ -341,20 +63,10 @@ error_code(type_error) -> 400; error_code(not_found) -> 404; error_code(internal) -> 500; error_code(Other) -> - ?WARNING_MSG("Unknown error identifier \"~p\". See mongoose_commands:errortype() for allowed values.", [Other]), + ?WARNING_MSG("Unknown error type \"~p\". " + "See mongoose_api_common:errortype() for allowed values.", [Other]), 500. -action_to_method(read) -> <<"GET">>; -action_to_method(update) -> <<"PUT">>; -action_to_method(delete) -> <<"DELETE">>; -action_to_method(create) -> <<"POST">>; -action_to_method(_) -> undefined. - -method_to_action(<<"GET">>) -> read; -method_to_action(<<"POST">>) -> create; -method_to_action(<<"PUT">>) -> update; -method_to_action(<<"DELETE">>) -> delete. - %%-------------------------------------------------------------------- %% Authorization %%-------------------------------------------------------------------- diff --git a/src/mongoose_commands.erl b/src/mongoose_commands.erl deleted file mode 100644 index 2b5098ded25..00000000000 --- a/src/mongoose_commands.erl +++ /dev/null @@ -1,693 +0,0 @@ -%% @doc Mongoose version of command management -%% The following is loosely based on old ejabberd_commands implementation, -%% with some modification related to type check, permission control -%% and the likes. -%% -%% This is a central registry of commands which can be exposed via -%% REST, XMPP as ad-hoc commands or in any other way. Any module can -%% define its commands and register them here. -%% -%% ==== Usage ==== -%% -%% A module defines a list of commands; a command definition is a proplist -%% with the following keys: -%% name :: atom() -%% name of the command by which we refer to it -%% category :: binary() -%% this defines what group the command belongs to, like user, chatroom etc -%% desc :: binary() -%% long description -%% module :: module() -%% module to call -%% function :: atom() -%% function to call -%% action :: command_action() -%% so that the HTTP side can decide which verb to require -%% args = [] :: [argspec()] -%% Type spec - see below; this is both for introspection and type check on call. Args spec is more -%% limited then return, it has to be a list of named arguments, like [{id, integer}, {msg, binary}] -%% security_policy = [atom()] (optional) -%% permissions required to run this command, defaults to [admin] -%% result :: argspec() -%% Type spec of return value of the function to call; execute/3 eventually returns {ok, result} -%% identifiers :: [atom()] (optional, required in 'update' commands) -%% optargs :: [{atom(), type(), term()] (optional args with type and default value. -%% Then a command is called, it fills missing arguments with values from here. -%% We have then two arities: arity of a command, which is only its required arguments, -%% and arity of the function to be called, which is required args + optional args. -%% -%% You can ignore return value of the target func by specifying return value as {result, ok}. The -%% execute/3 will then always return just 'ok' (or error). -%% -%% If action is 'update' then it MUST specify which args are to be used as identifiers of object -%% to update. It has no effect on how the engine does its job, but may be used by client code -%% to enforce proper structure of request. (this is bad programming practice but we didn't have -%% a better idea, we had to solve it for REST API) -%% -%% Commands are registered here upon the module's initialisation -%% (the module has to explicitly call mongoose_commands:register_commands/1 -%% func, it doesn't happen automagically), also should be unregistered when module -%% terminates. -%% -%% Commands are executed by calling mongoose_commands:execute/3 method. This -%% can return: -%% {ok, Result} -%% {error, denied, Msg} if user has no permission -%% {error, not_implemented, Msg} -%% {error, type_error, Msg} if either arguments or return value does not match -%% {error, internal, Msg} if an exception was caught -%% -%% ==== Type check ==== -%% -%% A command's definition includes specification of it arguments; when -%% it is called, arguments are check for compatibility. Examples of specs -%% and compliant arguments: -%% -%% ``` -%% a single type spec -%% integer 2 -%% binary <<"zzz">> -%% atom brrr -%% a list of arbitrary length, of a given type -%% [integer] [] -%% [integer] [1] -%% [integer] [1, 2, 3, 4] -%% a list of anything -%% [] -%% a named argument (name is only for clarity) -%% {msg, binary} <<"zzz">> -%% a tuple of args -%% {integer, binary, float} {1, <<"2">>, 3.0} -%% a tuple of named args -%% {{x, integer}, {y, binary}} {1, <<"bbb">>} -%% ''' -%% -%% Arg specification is used at call-time for control, and also for introspection -%% (see list/1, list/2, mongoose_commands:get_command/2 and args/1) -%% -%% Return value is also specified, and this is a bit tricky: command definition -%% contains spec of return value - what the target func returns should comply to it. -%% The registry, namely execute/3, returns a tuple {ok, ValueReturnedByTheFunction} -%% If return value is defined as 'ok' then whatever target func returns is ignored. -%% This is mostly to make a distinction between 'create' actions which actually create something -%% and return its identifier and those 'lame creators' which cause some action to be done and -%% something written to dbase (exemplum: sending a message), but there is no accessible resource. -%% -%% Called function may also return a tuple {error, term()}, this is returned by the registry -%% as {error, internal, Msg::binary()} -%% -%% ==== Permission control ==== -%% -%% First argument to every function exported from this module is always -%% a user. If you call it from trusted place, you can pass 'admin' here and -%% the whole permission check is skipped. Otherwise, pass #jid record. -%% -%% A command MAY define a security policy to be applied -%% (and this is not yet designed) -%% If it doesn't, then the command is accessible to 'admin' calls only. -%% - --module(mongoose_commands). --author("bartlomiej.gorny@erlang-solutions.com"). --include("mongoose.hrl"). --include("jlib.hrl"). - --record(mongoose_command, - { - %% name of the command by which we refer to it - name :: atom(), - %% groups commands related to the same functionality (user managment, messages/archive) - category :: binary(), - %% optimal subcategory - subcategory = undefined :: undefined | binary(), - %% long description - desc :: binary(), - %% module to call - module :: module(), - %% function to call - function :: atom(), - %% so that the HTTP side can decide which verb to require - action :: action(), - %% this is both for introspection and type check on call - args = [] :: [argspec()], - %% arg which has a default value and is optional - optargs = [] :: [optargspec()], - %% internal use - caller_pos :: integer(), - %% resource identifiers, a subset of args - identifiers = [] :: [atom()], - %% permissions required to run this command - security_policy = [admin] :: security(), - %% what the called func should return; if ok then return of called function is ignored - result :: argspec()|ok - }). - --opaque t() :: #mongoose_command{}. --type caller() :: admin | binary() | user. --type action() :: create | read | update | delete. %% just basic CRUD; sending a mesage is 'create' - --type typedef() :: [typedef_basic()] | typedef_basic(). - --type typedef_basic() :: boolean | integer | binary | float. %% most basic primitives, string is a binary - --type argspec() :: typedef() - | {atom(), typedef()} %% a named argument - | {argspec()} % a tuple of a few args (can be of any size) - | [typedef()]. % a list, but one element - --type optargspec() :: {atom(), typedef(), term()}. % name, type, default value - --type security() :: [admin | user]. %% later acl option will be added - --type errortype() :: denied | not_implemented | bad_request | type_error | not_found | internal. --type errorreason() :: term(). - --type args() :: [{atom(), term()}] | map(). --type failure() :: {error, errortype(), errorreason()}. --type success() :: ok | {ok, term()}. - --export_type([t/0]). --export_type([caller/0]). --export_type([action/0]). --export_type([argspec/0]). --export_type([optargspec/0]). --export_type([errortype/0]). --export_type([errorreason/0]). --export_type([failure/0]). - --type command_properties() :: [{atom(), term()}]. - -%%%% API - --export([check_type/3]). --export([init/0]). - --export([register/1, - unregister/1, - list/1, - list/2, - list/3, - list/4, - register_commands/1, - unregister_commands/1, - new/1, - get_command/2, - execute/3, - name/1, - category/1, - subcategory/1, - desc/1, - args/1, - optargs/1, - arity/1, - func_arity/1, - identifiers/1, - action/1, - result/1 - ]). - --ignore_xref([check_type/3, func_arity/1, get_command/2, list/3, new/1, - register_commands/1, unregister_commands/1, result/1]). - -%% @doc creates new command object based on provided proplist --spec new(command_properties()) -> t(). -new(Props) -> - Fields = record_info(fields, mongoose_command), - Lst = check_command([], Props, Fields), - RLst = lists:reverse(Lst), - Cmd = list_to_tuple([mongoose_command|RLst]), - check_identifiers(Cmd#mongoose_command.action, - Cmd#mongoose_command.identifiers, - Cmd#mongoose_command.args), - % store position of caller in args (if present) - Cmd#mongoose_command{caller_pos = locate_caller(Cmd#mongoose_command.args)}. - - -%% @doc Register mongoose commands. This can be run by any module that wants its commands exposed. --spec register([command_properties()]) -> ok. -register(Cmds) -> - Commands = [new(C) || C <- Cmds], - register_commands(Commands). - -%% @doc Unregister mongoose commands. Should be run when module is unloaded. --spec unregister([command_properties()]) -> ok. -unregister(Cmds) -> - Commands = [new(C) || C <- Cmds], - unregister_commands(Commands). - -%% @doc List commands, available for this user. --spec list(caller()) -> [t()]. -list(U) -> - list(U, any, any, any). - -%% @doc List commands, available for this user, filtered by category. --spec list(caller(), binary() | any) -> [t()]. -list(U, C) -> - list(U, C, any, any). - -%% @doc List commands, available for this user, filtered by category and action. --spec list(caller(), binary() | any, atom()) -> [t()]. -list(U, Category, Action) -> - list(U, Category, Action, any). - -%% @doc List commands, available for this user, filtered by category, action and subcategory --spec list(caller(), binary() | any, atom(), binary() | any | undefined) -> [t()]. -list(U, Category, Action, SubCategory) -> - CL = command_list(Category, Action, SubCategory), - lists:filter(fun(C) -> is_available_for(U, C) end, CL). - -%% @doc Get command definition, if allowed for this user. --spec get_command(caller(), atom()) -> t(). -get_command(Caller, Name) -> - case ets:lookup(mongoose_commands, Name) of - [C] -> - case is_available_for(Caller, C) of - true -> - C; - false -> - {error, denied, <<"Command not available">>} - end; - [] -> {error, not_implemented, <<"Command not implemented">>} - end. - -%% accessors --spec name(t()) -> atom(). -name(Cmd) -> - Cmd#mongoose_command.name. - --spec category(t()) -> binary(). -category(Cmd) -> - Cmd#mongoose_command.category. - --spec subcategory(t()) -> binary() | undefined. -subcategory(Cmd) -> - Cmd#mongoose_command.subcategory. - --spec desc(t()) -> binary(). -desc(Cmd) -> - Cmd#mongoose_command.desc. - --spec args(t()) -> term(). -args(Cmd) -> - Cmd#mongoose_command.args. - --spec optargs(t()) -> term(). -optargs(Cmd) -> - Cmd#mongoose_command.optargs. - --spec identifiers(t()) -> [atom()]. -identifiers(Cmd) -> - Cmd#mongoose_command.identifiers. - --spec action(t()) -> action(). -action(Cmd) -> - Cmd#mongoose_command.action. - --spec result(t()) -> term(). -result(Cmd) -> - Cmd#mongoose_command.result. - --spec arity(t()) -> integer(). -arity(Cmd) -> - length(Cmd#mongoose_command.args). - --spec func_arity(t()) -> integer(). -func_arity(Cmd) -> - length(Cmd#mongoose_command.args) + length(Cmd#mongoose_command.optargs). - -%% @doc Command execution. --spec execute(caller(), atom() | t(), args()) -> - success() | failure(). -execute(Caller, Name, Args) when is_atom(Name) -> - case ets:lookup(mongoose_commands, Name) of - [Command] -> execute_command(Caller, Command, Args); - [] -> {error, not_implemented, {command_not_supported, Name, sizeof(Args)}} - end; -execute(Caller, #mongoose_command{name = Name}, Args) -> - execute(Caller, Name, Args). - -init() -> - ets:new(mongoose_commands, [named_table, set, public, - {keypos, #mongoose_command.name}]). - -%%%% end of API --spec register_commands([t()]) -> ok. -register_commands(Commands) -> - lists:foreach( - fun(Command) -> - check_registration(Command), %% may throw - ets:insert_new(mongoose_commands, Command), - mongoose_hooks:register_command(Command), - ok - end, - Commands). - --spec unregister_commands([t()]) -> ok. -unregister_commands(Commands) -> - lists:foreach( - fun(Command) -> - ets:delete_object(mongoose_commands, Command), - mongoose_hooks:unregister_command(Command) - end, - Commands). - --spec execute_command(caller(), atom() | t(), args()) -> - success() | failure(). -execute_command(Caller, Command, Args) -> - try check_and_execute(Caller, Command, Args) of - ignore_result -> - ok; - {error, Type, Reason} -> - {error, Type, Reason}; - {ok, Res} -> - {ok, Res} - catch - % admittedly, not the best style of coding, in Erlang at least. But we have to do plenty - % of various checks, and in absence of something like Elixir's "with" construct we are - % facing a choice between throwing stuff or using some more or less tortured syntax - % to chain these checks. - throw:{Type, Reason} -> - {error, Type, Reason}; - Class:Reason:Stacktrace -> - Err = #{what => command_failed, - command_name => Command#mongoose_command.name, - caller => Caller, args => Args, - class => Class, reason => Reason, stacktrace => Stacktrace}, - ?LOG_ERROR(Err), - {error, internal, mongoose_lib:term_to_readable_binary(Err)} - end. - -add_defaults(Args, Opts) when is_map(Args) -> - COpts = [{K, V} || {K, _, V} <- Opts], - Missing = lists:subtract(proplists:get_keys(Opts), maps:keys(Args)), - lists:foldl(fun(K, Ar) -> maps:put(K, proplists:get_value(K, COpts), Ar) end, - Args, Missing). - -% @doc This performs many checks - types, permissions etc, may throw one of many exceptions -%% returns what the func returned or just ok if command spec tells so --spec check_and_execute(caller(), t(), args()) -> success() | failure() | ignore_result. -check_and_execute(Caller, Command, Args) when is_map(Args) -> - Args1 = add_defaults(Args, Command#mongoose_command.optargs), - ArgList = maps_to_list(Args1, Command#mongoose_command.args, Command#mongoose_command.optargs), - check_and_execute(Caller, Command, ArgList); -check_and_execute(Caller, Command, Args) -> - % check permissions - case is_available_for(Caller, Command) of - true -> - ok; - false -> - throw({denied, "Command not available for this user"}) - end, - % check caller (if it is given in args, and the engine is called by a 'real' user, then it - % must match - check_caller(Caller, Command, Args), - % check args - % this is the 'real' spec of command - optional args included - FullSpec = Command#mongoose_command.args - ++ [{K, T} || {K, T, _} <- Command#mongoose_command.optargs], - SpecLen = length(FullSpec), - ALen = length(Args), - case SpecLen =/= ALen of - true -> - type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]); - _ -> ok - end, - [check_type(argument, S, A) || {S, A} <- lists:zip(FullSpec, Args)], - % run command - Res = apply(Command#mongoose_command.module, Command#mongoose_command.function, Args), - case Res of - {error, Type, Reason} -> - {error, Type, Reason}; - _ -> - case Command#mongoose_command.result of - ok -> - ignore_result; - ResSpec -> - check_type(return, ResSpec, Res), - {ok, Res} - end - end. - -check_type(_, ok, _) -> - ok; -check_type(_, A, A) -> - true; -check_type(_, {_Name, boolean}, Value) when is_boolean(Value) -> - true; -check_type(Mode, {Name, boolean}, Value) -> - type_error(Mode, "For ~p expected boolean, got ~p", [Name, Value]); -check_type(_, {_Name, binary}, Value) when is_binary(Value) -> - true; -check_type(Mode, {Name, binary}, Value) -> - type_error(Mode, "For ~p expected binary, got ~p", [Name, Value]); -check_type(_, {_Name, integer}, Value) when is_integer(Value) -> - true; -check_type(Mode, {Name, integer}, Value) -> - type_error(Mode, "For ~p expected integer, got ~p", [Name, Value]); -check_type(Mode, {_Name, [_] = LSpec}, Value) when is_list(Value) -> - check_type(Mode, LSpec, Value); -check_type(Mode, Spec, Value) when is_tuple(Spec) and not is_tuple(Value) -> - type_error(Mode, "~p is not a tuple", [Value]); -check_type(Mode, Spec, Value) when is_tuple(Spec) -> - compare_tuples(Mode, Spec, Value); -check_type(_, [_Spec], []) -> - true; -check_type(Mode, [Spec], [H|T]) -> - check_type(Mode, {none, Spec}, H), - check_type(Mode, [Spec], T); -check_type(_, [], [_|_]) -> - true; -check_type(_, [], []) -> - true; -check_type(Mode, Spec, Value) -> - type_error(Mode, "Catch-all: ~p vs ~p", [Spec, Value]). - -compare_tuples(Mode, Spec, Val) -> - Ssize = tuple_size(Spec), - Vsize = tuple_size(Val), - case Ssize of - Vsize -> - compare_lists(Mode, tuple_to_list(Spec), tuple_to_list(Val)); - _ -> - type_error(Mode, "Tuples of different size: ~p and ~p", [Spec, Val]) - end. - -compare_lists(_, [], []) -> - true; -compare_lists(Mode, [S|Sp], [V|Val]) -> - check_type(Mode, S, V), - compare_lists(Mode, Sp, Val). - -type_error(argument, Fmt, V) -> - throw({type_error, io_lib:format(Fmt, V)}); -type_error(return, Fmt, V) -> - throw({internal, io_lib:format(Fmt, V)}). - -check_identifiers(update, [], _) -> - baddef(identifiers, empty); -check_identifiers(update, Ids, Args) -> - check_identifiers(Ids, Args); -check_identifiers(_, _, _) -> - ok. - -check_identifiers([], _) -> - ok; -check_identifiers([H|T], Args) -> - case proplists:get_value(H, Args) of - undefined -> baddef(H, missing); - _ -> check_identifiers(T, Args) - end. - -check_command(Cmd, _PL, []) -> - Cmd; -check_command(Cmd, PL, [N|Tail]) -> - V = proplists:get_value(N, PL), - Val = check_value(N, V), - check_command([Val|Cmd], PL, Tail). - -check_value(name, V) when is_atom(V) -> - V; -check_value(category, V) when is_binary(V) -> - V; -check_value(subcategory, V) when is_binary(V) -> - V; -check_value(subcategory, undefined) -> - undefined; -check_value(desc, V) when is_binary(V) -> - V; -check_value(module, V) when is_atom(V) -> - V; -check_value(function, V) when is_atom(V) -> - V; -check_value(action, read) -> - read; -check_value(action, send) -> - send; -check_value(action, create) -> - create; -check_value(action, update) -> - update; -check_value(action, delete) -> - delete; -check_value(args, V) when is_list(V) -> - Filtered = [C || {C, _} <- V], - ArgCount = length(V), - case length(Filtered) of - ArgCount -> V; - _ -> baddef(args, V) - end; -check_value(security_policy, undefined) -> - [admin]; -check_value(security_policy, []) -> - baddef(security_policy, empty); -check_value(security_policy, V) when is_list(V) -> - lists:map(fun check_security_policy/1, V), - V; -check_value(result, undefined) -> - baddef(result, undefined); -check_value(result, V) -> - V; -check_value(identifiers, undefined) -> - []; -check_value(identifiers, V) -> - V; -check_value(optargs, undefined) -> - []; -check_value(optargs, V) -> - V; -check_value(caller_pos, _) -> - 0; -check_value(K, V) -> - baddef(K, V). - -%% @doc Known security policies -check_security_policy(user) -> - ok; -check_security_policy(admin) -> - ok; -check_security_policy(Other) -> - baddef(security_policy, Other). - -baddef(K, V) -> - throw({invalid_command_definition, io_lib:format("~p=~p", [K, V])}). - -command_list(Category, Action, SubCategory) -> - Cmds = [C || [C] <- ets:match(mongoose_commands, '$1')], - filter_commands(Category, Action, SubCategory, Cmds). - -filter_commands(any, any, _, Cmds) -> - Cmds; -filter_commands(Cat, any, _, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat]; -filter_commands(any, _, _, _) -> - throw({invalid_filter, ""}); -filter_commands(Cat, Action, any, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat, - C#mongoose_command.action == Action]; -filter_commands(Cat, Action, SubCategory, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat, - C#mongoose_command.action == Action, - C#mongoose_command.subcategory == SubCategory]. - - -%% @doc make sure the command may be registered -%% it may not if either (a) command of that name is already registered, -%% (b) there is a command in the same category and subcategory with the same action and arity -check_registration(Command) -> - Name = name(Command), - Cat = category(Command), - Act = action(Command), - Arity = arity(Command), - SubCat = subcategory(Command), - case ets:lookup(mongoose_commands, Name) of - [] -> - CatLst = list(admin, Cat), - FCatLst = [C || C <- CatLst, action(C) == Act, - subcategory(C) == SubCat, - arity(C) == Arity], - case FCatLst of - [] -> ok; - [C] -> - baddef("There is a command ~p in category ~p and subcategory ~p, action ~p", - [name(C), Cat, SubCat, Act]) - end; - Other -> - ?LOG_DEBUG(#{what => command_conflict, - text => <<"This command is already defined">>, - command_name => Name, registered => Other}), - ok - end. - -mapget(K, Map) -> - try maps:get(K, Map) of - V -> V - catch - error:{badkey, K} -> - type_error(argument, "Missing argument: ~p", [K]); - error:bad_key -> - type_error(argument, "Missing argument: ~p", [K]) - end. - -maps_to_list(Map, Args, Optargs) -> - SpecLen = length(Args) + length(Optargs), - ALen = maps:size(Map), - case SpecLen of - ALen -> ok; - _ -> type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]) - end, - [mapget(K, Map) || {K, _} <- Args] ++ [mapget(K, Map) || {K, _, _} <- Optargs]. - -%% @doc Main entry point for permission control - is this command available for this user -is_available_for(User, C) when is_binary(User) -> - is_available_for(jid:from_binary(User), C); -is_available_for(admin, _C) -> - true; -is_available_for(Jid, #mongoose_command{security_policy = Policies}) -> - apply_policies(Policies, Jid). - -%% @doc Check all security policies defined in the command - passes if any of them returns true -apply_policies([], _) -> - false; -apply_policies([P|Policies], Jid) -> - case apply_policy(P, Jid) of - true -> - true; - false -> - apply_policies(Policies, Jid) - end. - -%% @doc This is the only policy we know so far, but there will be others (like roles/acl control) -apply_policy(user, _) -> - true; -apply_policy(_, _) -> - false. - -locate_caller(L) -> - locate_caller(1, L). - -locate_caller(_I, []) -> - 0; -locate_caller(I, [{caller, _}|_]) -> - I; -locate_caller(I, [_|T]) -> - locate_caller(I + 1, T). - -check_caller(admin, _Command, _Args) -> - ok; -check_caller(_Caller, #mongoose_command{caller_pos = 0}, _Args) -> - % no caller in args - ok; -check_caller(Caller, #mongoose_command{caller_pos = CallerPos}, Args) -> - % check that server and user match (we don't care about resource) - ACaller = lists:nth(CallerPos, Args), - CallerJid = jid:from_binary(Caller), - ACallerJid = jid:from_binary(ACaller), - case jid:are_bare_equal(CallerJid, ACallerJid) of - true -> - ok; - _ -> - throw({denied, "Caller ids do not match"}) - end. - -sizeof(#{} = M) -> maps:size(M); -sizeof([_|_] = L) -> length(L). diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index c60646178f9..7b831a77887 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -24,7 +24,6 @@ packet_to_component/3, presence_probe_hook/5, push_notifications/4, - register_command/1, register_subhost/2, register_user/3, remove_user/3, @@ -34,7 +33,6 @@ set_vcard/3, unacknowledged_message/2, filter_unacknowledged_messages/3, - unregister_command/1, unregister_subhost/1, user_available_hook/2, user_ping_response/5, @@ -334,14 +332,6 @@ push_notifications(HostType, Acc, NotificationForms, Options) -> ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), run_hook_for_host_type(push_notifications, HostType, Acc, ParamsWithLegacyArgs). -%%% @doc The `register_command' hook is called when a command -%%% is registered in `mongoose_commands'. --spec register_command(Command) -> Result when - Command :: mongoose_commands:t(), - Result :: drop. -register_command(Command) -> - run_global_hook(register_command, Command, []). - %%% @doc The `register_subhost' hook is called when a component %%% is registered for ejabberd_router or a subdomain is added to mongoose_subdomain_core. -spec register_subhost(LDomain, IsHidden) -> Result when @@ -433,14 +423,6 @@ unacknowledged_message(Acc, JID) -> filter_unacknowledged_messages(HostType, Jid, Buffer) -> run_fold(filter_unacknowledged_messages, HostType, Buffer, #{jid => Jid}). -%%% @doc The `unregister_command' hook is called when a command -%%% is unregistered from `mongoose_commands'. --spec unregister_command(Command) -> Result when - Command :: mongoose_commands:t(), - Result :: drop. -unregister_command(Command) -> - run_global_hook(unregister_command, Command, []). - %%% @doc The `unregister_subhost' hook is called when a component %%% is unregistered from ejabberd_router or a subdomain is removed from mongoose_subdomain_core. -spec unregister_subhost(LDomain) -> Result when diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index efc7cf011fb..eee7434a57a 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -83,8 +83,7 @@ cowboy_host(Host) -> Host. configurable_handler_modules() -> [mod_websockets, mongoose_client_api, - mongoose_api, - mongoose_api_admin, mongoose_admin_api, + mongoose_api, mongoose_domain_handler, mongoose_graphql_cowboy_handler]. diff --git a/src/mongoose_lib.erl b/src/mongoose_lib.erl index 9625d4c8411..e3340a06700 100644 --- a/src/mongoose_lib.erl +++ b/src/mongoose_lib.erl @@ -10,7 +10,6 @@ -export([wait_until/2, wait_until/3]). -export([parse_ip_netmask/1]). --export([term_to_readable_binary/1]). -export([get_message_type/1, does_local_user_exist/3]). %% Private, just for warning @@ -156,9 +155,6 @@ parse_ip_netmask(IPStr, MaskStr) -> error end. -term_to_readable_binary(X) -> - iolist_to_binary(io_lib:format("~0p", [X])). - %% ------------------------------------------------------------------ %% does_local_user_exist %% ------------------------------------------------------------------ diff --git a/src/mongoose_stanza_helper.erl b/src/mongoose_stanza_helper.erl index 85aaa1212b3..ea1025c4f1c 100644 --- a/src/mongoose_stanza_helper.erl +++ b/src/mongoose_stanza_helper.erl @@ -1,7 +1,6 @@ -module(mongoose_stanza_helper). -export([build_message/3]). -export([build_message_with_headline/3]). --export([parse_from_to/2]). -export([get_last_messages/4]). -export([route/4]). @@ -37,33 +36,6 @@ maybe_cdata_elem(Name, Text) when is_binary(Text) -> cdata_elem(Name, Text) when is_binary(Name), is_binary(Text) -> #xmlel{name = Name, children = [#xmlcdata{content = Text}]}. --spec parse_from_to(jid:jid() | binary() | undefined, jid:jid() | binary() | undefined) -> - {ok, jid:jid(), jid:jid()} | {error, missing} | {error, type_error, string()}. -parse_from_to(F, T) when F == undefined; T == undefined -> - {error, missing}; -parse_from_to(F, T) -> - case parse_jid_list([F, T]) of - {ok, [Fjid, Tjid]} -> {ok, Fjid, Tjid}; - E -> E - end. - --spec parse_jid_list(BinJids :: [binary()]) -> {ok, [jid:jid()]} | {error, type_error, string()}. -parse_jid_list([_ | _] = BinJids) -> - Jids = lists:map(fun parse_jid/1, BinJids), - case [Msg || {error, Msg} <- Jids] of - [] -> {ok, Jids}; - Errors -> {error, type_error, lists:join("; ", Errors)} - end. - --spec parse_jid(binary() | jid:jid()) -> jid:jid() | {error, string()}. -parse_jid(#jid{} = Jid) -> - Jid; -parse_jid(Jid) when is_binary(Jid) -> - case jid:from_binary(Jid) of - error -> {error, io_lib:format("Invalid jid: ~p", [Jid])}; - B -> B - end. - -spec get_last_messages(Caller :: jid:jid(), Limit :: non_neg_integer(), With :: null | jid:jid(), diff --git a/src/muc_light/mod_muc_light_commands.erl b/src/muc_light/mod_muc_light_commands.erl deleted file mode 100644 index 129860b283b..00000000000 --- a/src/muc_light/mod_muc_light_commands.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%============================================================================== -%% Copyright 2016 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% Author: Joseph Yiasemides -%% Description: Administration commands for MUC Light -%%============================================================================== - --module(mod_muc_light_commands). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/2, stop/1, supported_features/0]). - --export([create_unique_room/4]). --export([create_identifiable_room/5]). --export([send_message/4]). --export([invite_to_room/4]). --export([delete_room/2]). --export([change_room_config/5]). - --ignore_xref([create_identifiable_room/5, create_unique_room/4, delete_room/2, - invite_to_room/4, send_message/4, change_room_config/5]). - -%%-------------------------------------------------------------------- -%% `gen_mod' callbacks -%%-------------------------------------------------------------------- - -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%%-------------------------------------------------------------------- -%% Interface descriptions -%%-------------------------------------------------------------------- - -commands() -> - - [ - [{name, create_muc_light_room}, - {category, <<"muc-lights">>}, - {desc, <<"Create a MUC Light room with unique username part in JID.">>}, - {module, ?MODULE}, - {function, create_unique_room}, - {action, create}, - {identifiers, [domain]}, - {args, - [ - %% The parent `domain' under which MUC Light is - %% configured. - {domain, binary}, - {name, binary}, - {owner, binary}, - {subject, binary} - ]}, - {result, {name, binary}}], - - [{name, create_identifiable_muc_light_room}, - {category, <<"muc-lights">>}, - {desc, <<"Create a MUC Light room with user-provided username part in JID">>}, - {module, ?MODULE}, - {function, create_identifiable_room}, - {action, update}, - {identifiers, [domain]}, - {args, - [{domain, binary}, - {id, binary}, - {name, binary}, - {owner, binary}, - {subject, binary} - ]}, - {result, {id, binary}}], - - [{name, change_muc_light_room_configuration}, - {category, <<"muc-lights">>}, - {subcategory, <<"config">>}, - {desc, <<"Change configuration of MUC Light room.">>}, - {module, ?MODULE}, - {function, change_room_config}, - {action, update}, - {identifiers, [domain]}, - {args, - [ - {domain, binary}, - {id, binary}, - {name, binary}, - {user, binary}, - {subject, binary} - ]}, - {result, ok}], - - [{name, invite_to_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"participants">>}, - {desc, <<"Invite to a MUC Light room.">>}, - {module, ?MODULE}, - {function, invite_to_room}, - {action, create}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary}, - {sender, binary}, - {recipient, binary} - ]}, - {result, ok}], - - [{name, send_message_to_muc_light_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"messages">>}, - {desc, <<"Send a message to a MUC Light room.">>}, - {module, ?MODULE}, - {function, send_message}, - {action, create}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary}, - {from, binary}, - {body, binary} - ]}, - {result, ok}], - - [{name, delete_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"management">>}, - {desc, <<"Delete a MUC Light room.">>}, - {module, ?MODULE}, - {function, delete_room}, - {action, delete}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary} - ]}, - {result, ok}] - ]. - - -%%-------------------------------------------------------------------- -%% Internal procedures -%%-------------------------------------------------------------------- - --spec create_unique_room(jid:server(), jid:user(), jid:literal_jid(), binary()) -> - jid:literal_jid() | {error, not_found | denied, iolist()}. -create_unique_room(MUCServer, RoomName, Creator, Subject) -> - CreatorJID = jid:from_binary(Creator), - case mod_muc_light_api:create_room(MUCServer, CreatorJID, RoomName, Subject) of - {ok, #{jid := JID}} -> jid:to_binary(JID); - Error -> format_err_result(Error) - end. - --spec create_identifiable_room(jid:server(), jid:user(), binary(), - jid:literal_jid(), binary()) -> - jid:literal_jid() | {error, not_found | denied, iolist()}. -create_identifiable_room(MUCServer, Identifier, RoomName, Creator, Subject) -> - CreatorJID = jid:from_binary(Creator), - case mod_muc_light_api:create_room(MUCServer, Identifier, CreatorJID, RoomName, Subject) of - {ok, #{jid := JID}} -> jid:to_binary(JID); - Error -> format_err_result(Error) - end. - --spec invite_to_room(jid:server(), jid:user(), jid:literal_jid(), jid:literal_jid()) -> - ok | {error, not_found | denied, iolist()}. -invite_to_room(MUCServer, RoomID, Sender, Recipient) -> - SenderJID = jid:from_binary(Sender), - RecipientJID = jid:from_binary(Recipient), - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:invite_to_room(RoomJID, SenderJID, RecipientJID), - format_result_no_msg(Result). - --spec change_room_config(jid:server(), jid:user(), binary(), jid:literal_jid(), binary()) -> - ok | {error, not_found | denied, iolist()}. -change_room_config(MUCServer, RoomID, RoomName, User, Subject) -> - UserJID = jid:from_binary(User), - RoomJID = jid:make_bare(RoomID, MUCServer), - Config = #{<<"roomname">> => RoomName, <<"subject">> => Subject}, - Result = mod_muc_light_api:change_room_config(RoomJID, UserJID, Config), - format_result_no_msg(Result). - --spec send_message(jid:server(), jid:user(), jid:literal_jid(), binary()) -> - ok | {error, not_found | denied, iolist()}. -send_message(MUCServer, RoomID, Sender, Message) -> - SenderJID = jid:from_binary(Sender), - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:send_message(RoomJID, SenderJID, Message), - format_result_no_msg(Result). - --spec delete_room(jid:server(), jid:user()) -> - ok | {error, not_found, iolist()}. -delete_room(MUCServer, RoomID) -> - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:delete_room(RoomJID), - format_result_no_msg(Result). - -format_result_no_msg({ok, _}) -> ok; -format_result_no_msg(Res) -> format_err_result(Res). - -format_err_result({ResStatus, Msg}) when room_not_found =:= ResStatus; - muc_server_not_found =:= ResStatus -> - {error, not_found, Msg}; -format_err_result({ResStatus, Msg}) when already_exist =:= ResStatus; - not_allowed =:= ResStatus; - not_room_member =:= ResStatus -> - {error, denied, Msg}; -format_err_result({_, Reason}) -> {error, internal, Reason}. diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 7d7f5ce5589..2fcca06f7b7 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -126,7 +126,7 @@ get_api() -> [#{report_name => http_api, key => Api, value => enabled} || Api <- ApiList]. filter_unknown_api(ApiList) -> - AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_api_admin, + AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_admin_api, mongoose_domain_handler, mod_bosh, mod_websockets], [Api || Api <- ApiList, lists:member(Api, AllowedToReport)]. diff --git a/test/commands_SUITE.erl b/test/commands_SUITE.erl deleted file mode 100644 index 68f49b7d3e9..00000000000 --- a/test/commands_SUITE.erl +++ /dev/null @@ -1,627 +0,0 @@ -%% @doc This suite tests both old ejabberd_commands module, which is slowly getting deprecated, -%% and the new mongoose_commands implementation. --module(commands_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("exml/include/exml.hrl"). --include_lib("eunit/include/eunit.hrl"). --include("ejabberd_commands.hrl"). --include("jlib.hrl"). - --define(PRT(X, Y), ct:pal("~p: ~p", [X, Y])). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% suite configuration -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -all() -> - [ - {group, old_commands}, - {group, new_commands} - ]. - -groups() -> - [ - {old_commands, [sequence], - [old_list, - old_exec, - old_access_ctl - ] - }, - {new_commands, [sequence], - [new_type_checker, - new_reg_unreg, - new_failedreg, - new_list, - new_execute, - different_types, - errors_are_readable - ] - } - ]. - -init_per_suite(C) -> - application:ensure_all_started(jid), - ok = mnesia:start(), - C. - -end_per_suite(_) -> - mnesia:stop(), - mnesia:delete_schema([node()]), - ok. - -init_per_group(old_commands, C) -> - Pid = spawn(fun ec_holder/0), - [{helper_proc, Pid} | C]; -init_per_group(new_commands, C) -> - Pid = spawn(fun mc_holder/0), - [{helper_proc, Pid} | C]. - -end_per_group(old_commands, C) -> - ejabberd_commands:unregister_commands(commands_old()), - stop_helper_proc(C), - C; -end_per_group(new_commands, C) -> - mongoose_commands:unregister(commands_new()), - stop_helper_proc(C), - C. - -stop_helper_proc(C) -> - Pid = proplists:get_value(helper_proc, C), - Pid ! stop. - -init_per_testcase(_, C) -> - [mongoose_config:set_opt(Key, Value) || {Key, Value} <- opts()], - meck:new(ejabberd_auth_dummy, [non_strict]), - meck:expect(ejabberd_auth_dummy, get_password_s, fun(_, _) -> <<"">> end), - meck:new(mongoose_domain_api), - meck:expect(mongoose_domain_api, get_domain_host_type, fun(H) -> {ok, H} end), - C. - -end_per_testcase(_, _C) -> - [mongoose_config:unset_opt(Key) || {Key, _Value} <- opts()], - meck:unload(). - -opts() -> - [{{auth, <<"localhost">>}, #{methods => [dummy]}}, - {{access, <<"localhost">>}, #{experts_only => [#{acl => coder, value => allow}, - #{acl => manager, value => allow}, - #{acl => all, value => deny}]}}, - {{acl, <<"localhost">>}, #{coder => [#{user => <<"zenek">>, match => current_domain}]}}]. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% test methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - -old_list(_C) -> - %% list - Rlist = ejabberd_commands:list_commands(), - {command_one, _, "do nothing and return"} = proplists:lookup(command_one, Rlist), - %% get definition - Rget = ejabberd_commands:get_command_definition(command_one), - % we should get back exactly the definition we provided - [Cone | _] = commands_old(), - Cone = Rget, - %% get interface - {Argspec, Retspec} = ejabberd_commands:get_command_format(command_one), - [{msg, binary}] = Argspec, - {res, restuple} = Retspec, - %% list by tags - Tagcomm = ejabberd_commands:get_tags_commands(), - ?assertEqual(length(proplists:get_value("one", Tagcomm)), 1), - ?assertEqual(length(proplists:get_value("two", Tagcomm)), 2), - ?assertEqual(length(proplists:get_value("three", Tagcomm)), 1), - ok. - -old_exec(_C) -> - %% execute - <<"bzzzz">> = ejabberd_commands:execute_command(command_one, [<<"bzzzz">>]), - Res = ejabberd_commands:execute_command(command_one, [123]), - ?PRT("invalid type ignored", Res), %% there is no arg type check - Res2 = ejabberd_commands:execute_command(command_two, [123]), - ?PRT("invalid return type ignored", Res2), %% nor return - %% execute unknown command - {error, command_unknown} = ejabberd_commands:execute_command(command_seven, [123]), - ok. - -old_access_ctl(_C) -> - %% with no auth method it is all fine - checkauth(true, #{}, noauth), - %% noauth fails if first item is not 'all' (users) - checkauth(account_unprivileged, #{none => command_rules(all)}, noauth), - %% if here we allow all commands to noauth - checkauth(true, #{all => command_rules(all)}, noauth), - %% and here only command_one - checkauth(true, #{all => command_rules([command_one])}, noauth), - %% so this'd fail - checkauth(account_unprivileged, #{all => command_rules([command_two])}, noauth), - % now we provide a role name, this requires a user and triggers password and acl check - % this fails because password is bad - checkauth(invalid_account_data, #{some_acl_role => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"bbb">>}), - % this, because of acl - checkauth(account_unprivileged, #{some_acl_role => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"">>}), - % and this should work, because we define command_one as available to experts only, while acls in config - % (see ggo/1) state that experts-only funcs are available to coders and managers, and zenek is a coder, gah. - checkauth(true, #{experts_only => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"">>}), - ok. - -new_type_checker(_C) -> - true = t_check_type({msg, binary}, <<"zzz">>), - true = t_check_type({msg, integer}, 127), - {false, _} = t_check_type({{a, binary}, {b, integer}}, 127), - true = t_check_type({{a, binary}, {b, integer}}, {<<"z">>, 127}), - true = t_check_type({ok, {msg, integer}}, {ok, 127}), - true = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), - {false, _} = t_check_type({k, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), - {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, "z"}), - {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>, 333}), - true = t_check_type([integer], []), - true = t_check_type([integer], [1, 2, 3]), - {false, _} = t_check_type([integer], [1, <<"z">>, 3]), - true = t_check_type([], [1, 2, 3]), - true = t_check_type([], []), - true = t_check_type({msg, boolean}, true), - true = t_check_type({msg, boolean}, false), - {false, _} = t_check_type({msg, boolean}, <<"true">>), - ok. - -t_check_type(Spec, Value) -> - R = try mongoose_commands:check_type(argument, Spec, Value) of - true -> true - catch - E -> - {false, E} - end, - R. - -new_reg_unreg(_C) -> - L1 = length(commands_new()), - L2 = L1 + length(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L1), - mongoose_commands:register(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L2), - mongoose_commands:unregister(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L1), - ok. - -failedreg([]) -> ok; -failedreg([Cmd|Tail]) -> - ?assertThrow({invalid_command_definition, _}, mongoose_commands:register([Cmd])), - failedreg(Tail). - -new_failedreg(_C) -> - failedreg(commands_new_lame()). - - -new_list(_C) -> - %% for admin - Rlist = mongoose_commands:list(admin), - [Cmd] = [C || C <- Rlist, mongoose_commands:name(C) == command_one], - command_one = mongoose_commands:name(Cmd), - <<"do nothing and return">> = mongoose_commands:desc(Cmd), - %% list by category - [_] = mongoose_commands:list(admin, <<"user">>), - [] = mongoose_commands:list(admin, <<"nocategory">>), - %% list by category and action - [_] = mongoose_commands:list(admin, <<"user">>, read), - [] = mongoose_commands:list(admin, <<"user">>, update), - %% get definition - Rget = mongoose_commands:get_command(admin, command_one), - command_one = mongoose_commands:name(Rget), - read = mongoose_commands:action(Rget), - [] = mongoose_commands:identifiers(Rget), - {error, denied, _} = mongoose_commands:get_command(ujid(), command_one), - %% list for a user - Ulist = mongoose_commands:list(ujid()), - [UCmd] = [UC || UC <- Ulist, mongoose_commands:name(UC) == command_foruser], - - command_foruser = mongoose_commands:name(UCmd), - URget = mongoose_commands:get_command(ujid(), command_foruser), - command_foruser = mongoose_commands:name(URget), - ok. - - -new_execute(_C) -> - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, [<<"bzzzz">>]), - Cmd = mongoose_commands:get_command(admin, command_one), - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, Cmd, [<<"bzzzz">>]), - %% call with a map - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>}), - %% command which returns just ok - ok = mongoose_commands:execute(admin, command_noreturn, [<<"bzzzz">>]), - %% this user has no permissions - {error, denied, _} = mongoose_commands:execute(ujid(), command_one, [<<"bzzzz">>]), - %% command is not registered - {error, not_implemented, _} = mongoose_commands:execute(admin, command_seven, [<<"bzzzz">>]), - %% invalid arguments - {error, type_error, _} = mongoose_commands:execute(admin, command_one, [123]), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, []), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => 123}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{notthis => <<"bzzzz">>}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>, redundant => 123}), - %% backend func throws exception - {error, internal, _} = mongoose_commands:execute(admin, command_one, [<<"throw">>]), - %% backend func returns error - {error, internal, <<"byleco">>} = mongoose_commands:execute(admin, command_one, [<<"error">>]), - % user executes his command - {ok, <<"bzzzz">>} = mongoose_commands:execute(ujid(), command_foruser, #{msg => <<"bzzzz">>}), - % a caller arg - % called by admin - {ok, <<"admin@localhost/zbzzzz">>} = mongoose_commands:execute(admin, - command_withcaller, - #{caller => <<"admin@localhost/z">>, - msg => <<"bzzzz">>}), - % called by user - {ok, <<"zenek@localhost/zbzzzz">>} = mongoose_commands:execute(<<"zenek@localhost">>, - command_withcaller, - #{caller => <<"zenek@localhost/z">>, - msg => <<"bzzzz">>}), - % call by user but jids do not match - {error, denied, _} = mongoose_commands:execute(<<"wacek@localhost">>, - command_withcaller, - #{caller => <<"zenek@localhost/z">>, - msg => <<"bzzzz">>}), - {ok, 30} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>}), - {ok, 18} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>, value => 6}), - ok. - -different_types(_C) -> - mongoose_commands:register(commands_new_temp2()), - {ok, <<"response1">>} = mongoose_commands:execute(admin, command_two, [10, 15]), - {ok, <<"response2">>} = mongoose_commands:execute(admin, command_three, [10, <<"binary">>]), - mongoose_commands:unregister(commands_new_temp2()), - ok. - -errors_are_readable(_C) -> - {error, internal, TextBin} = mongoose_commands:execute(admin, make_error, [<<"oops">>]), - Map = parse_binary_term(TextBin), - [<<"oops">>] = maps:get(args, Map), - admin = maps:get(caller, Map), - error = maps:get(class, Map), - make_error = maps:get(command_name, Map), - <<"oops">> = maps:get(reason, Map), - [_|_] = maps:get(stacktrace, Map), - command_failed = maps:get(what, Map), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% definitions -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -commands_new() -> - [ - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_noreturn}, - {category, <<"message">>}, - {desc, <<"do nothing and return nothing">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, create}, - {args, [{msg, binary}]}, - {result, ok} - ], - [ - {name, command_foruser}, - {category, <<"another">>}, - {desc, <<"this is available for a user">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {security_policy, [user]}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_withoptargs}, - {category, <<"yetanother">>}, - {desc, <<"this is available for a user">>}, - {module, ?MODULE}, - {function, cmd_one_withvalue}, - {action, read}, - {security_policy, [user]}, - {args, [{msg, binary}]}, - {optargs, [{value, integer, 10}]}, - {result, {nvalue, integer}} - ], - [ - {name, command_withcaller}, - {category, <<"another">>}, - {desc, <<"this has a 'caller' argument, returns caller ++ msg">>}, - {module, ?MODULE}, - {function, cmd_concat}, - {action, create}, - {security_policy, [user]}, - {args, [{caller, binary}, {msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, make_error}, - {category, <<"testing">>}, - {desc, <<"Just to test an error">>}, - {module, erlang}, - {function, error}, - {action, read}, - {args, [{error, binary}]}, - {result, []} - ] - ]. - -commands_new_temp() -> - %% this is to check registering/unregistering commands - [ - [ - {name, command_temp}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, create}, % different action - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one_arity}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}, {whatever, integer}]}, % different arity - {result, {msg, binary}} - ], - [ - {name, command_one_two}, - {category, <<"user">>}, - {subcategory, <<"rosters">>}, % has subcategory - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_temp2}, - {category, <<"user">>}, - {desc, <<"this one specifies identifiers">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [ident]}, - {args, [{ident, integer}, {msg, binary}]}, - {result, {msg, binary}} - ] - ]. - -commands_new_temp2() -> - %% This is for extra test with different arg types - [ - [ - {name, command_two}, - {category, <<"animals">>}, - {desc, <<"some">>}, - {module, ?MODULE}, - {function, the_same_types}, - {action, read}, - {args, [{one, integer}, {two, integer}]}, - {result, {msg, binary}} - ], - [ - {name, command_three}, - {category, <<"music">>}, - {desc, <<"two args, different types">>}, - {module, ?MODULE}, - {function, different_types}, - {action, read}, - {args, [{one, integer}, {two, binary}]}, - {result, {msg, binary}} - ] - ]. - -commands_new_lame() -> - [ - [ - {name, command_one} % missing values - ], - [ - {name, command_one}, - {category, []} %% should be binary - ], - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, andnowforsomethingcompletelydifferent} %% not one of allowed values - ], - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, delete}, - {args, [{msg, binary}, integer]}, %% args have to be a flat list of named arguments - {result, {msg, binary}} - ], -%% We do not crash if command is already registered because some modules are loaded more then once -%% [ -%% {name, command_one}, %% everything is fine, but it is already registered -%% {category, another}, -%% {desc, "do nothing and return"}, -%% {module, ?MODULE}, -%% {function, cmd_one}, -%% {action, read}, -%% {args, [{msg, binary}]}, -%% {result, {msg, binary}} -%% ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, %% an 'update' command has to specify identifiers - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [1]}, %% ...and they must be atoms... - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [ident]}, %% ...which are present in args - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_seven}, %% name is different... - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, %% ...but another command with the same category and action and arity is already registered - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_seven}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, delete}, - {security_policy, [wrong]}, % invalid security definition - {args, [{msg, binary}]}, - {result, {msg, binary}} -%% ], -%% [ -%% {name, command_seven}, -%% {category, user}, -%% {desc, "do nothing and return"}, -%% {module, ?MODULE}, -%% {function, cmd_one}, -%% {action, delete}, -%% {security_policy, []}, % invalid security definition -%% {args, [{msg, binary}]}, -%% {result, {msg, binary}} - ] - ]. - -commands_old() -> - [ - #ejabberd_commands{name = command_one, tags = [one], - desc = "do nothing and return", - module = ?MODULE, function = cmd_one, - args = [{msg, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = command_two, tags = [two], - desc = "this returns wrong type", - module = ?MODULE, function = cmd_two, - args = [{msg, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = command_three, tags = [two, three], - desc = "do nothing and return", - module = ?MODULE, function = cmd_three, - args = [{msg, binary}], - result = {res, restuple}} - ]. - -cmd_one(<<"throw">>) -> - C = 12, - <<"A", C/binary>>; -cmd_one(<<"error">>) -> - {error, internal, <<"byleco">>}; -cmd_one(M) -> - M. - -cmd_one_withvalue(_Msg, Value) -> - Value * 3. - -cmd_two(M) -> - M. - -the_same_types(10, 15) -> - <<"response1">>; -the_same_types(_, _) -> - <<"wrong response">>. - -different_types(10, <<"binary">>) -> - <<"response2">>; -different_types(_, _) -> - <<"wrong content">>. - -cmd_concat(A, B) -> - <>. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% utilities -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% this is a bit stupid, but we need a process which would hold ets table -ec_holder() -> - ejabberd_commands:init(), - ejabberd_commands:register_commands(commands_old()), - receive - _ -> ok - end. - -mc_holder() -> - % we have to do it here to avoid race condition and random failures - {ok, Pid} = gen_hook:start_link(), - mongoose_commands:init(), - mongoose_commands:register(commands_new()), - receive - _ -> ok - end, - erlang:exit(Pid, kill). - -command_rules(Commands) -> - #{commands => Commands, argument_restrictions => #{}}. - -checkauth(true, AccessCommands, Auth) -> - B = <<"bzzzz">>, - B = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]); -checkauth(ErrMess, AccessCommands, Auth) -> - B = <<"bzzzz">>, - {error, ErrMess} = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]). - -ujid() -> - <<"zenek@localhost/k">>. -%% #jid{user = <<"zenek">>, server = <<"localhost">>, resource = "k", -%% luser = <<"zenek">>, lserver = <<"localhost">>, lresource = "k"}. - -parse_binary_term(TextBin) -> - {ok, Tokens, _} = erl_scan:string(binary_to_list(TextBin) ++ "."), - {ok, Abstract} = erl_parse:parse_exprs(Tokens), - {value, Value, _} = erl_eval:exprs(Abstract, erl_eval:new_bindings()), - Value. diff --git a/test/commands_backend_SUITE.erl b/test/commands_backend_SUITE.erl deleted file mode 100644 index a32593743d7..00000000000 --- a/test/commands_backend_SUITE.erl +++ /dev/null @@ -1,536 +0,0 @@ -%% @doc This suite tests both old ejabberd_commands module, which is slowly getting deprecated, -%% and the new mongoose_commands implementation. --module(commands_backend_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("eunit/include/eunit.hrl"). - --define(PORT, 5288). --define(HOST, "localhost"). --define(IP, {127,0,0,1}). - --type method() :: string(). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% suite configuration -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -backend_module() -> - mongoose_api_admin. - -all() -> - [ - {group, simple_backend}, - {group, get_advanced_backend}, - {group, post_advanced_backend}, - {group, delete_advanced_backend} - ]. - -groups() -> - [ - {simple_backend, [sequence], - [ - get_simple, - post_simple, - delete_simple, - put_simple - ] - }, - {get_advanced_backend, [sequence], - [ - get_two_args, - get_wrong_path, - get_wrong_arg_number, - get_no_command, - get_wrong_arg_type - ] - }, - {post_advanced_backend, [sequence], - [ - post_simple_with_subcategory, - post_different_arg_order, - post_wrong_arg_number, - post_wrong_arg_name, - post_wrong_arg_type, - post_no_command - ] - }, - {delete_advanced_backend, [sequence], - [ - delete_wrong_arg_order, - delete_wrong_arg_types - ] - }, - {put_advanced_backend, [sequence], - [ - put_wrong_type, - put_wrong_param_type, - put_wrong_bind_type, - put_different_params_order, - put_wrong_binds_order, - put_too_less_params, - put_too_less_binds, - put_wrong_bind_name, - put_wrong_param_name - ] - } - ]. - -setup(Module) -> - meck:unload(), - meck:new(supervisor, [unstick, passthrough, no_link]), - meck:new(gen_hook, []), - meck:new(ejabberd_auth, []), - %% you have to meck some stuff to get it working.... - meck:expect(gen_hook, add_handler, fun(_, _, _, _, _) -> ok end), - meck:expect(gen_hook, run_fold, fun(_, _, _, _) -> {ok, ok} end), - spawn(fun mc_holder/0), - meck:expect(supervisor, start_child, - fun(mongoose_listener_sup, _) -> {ok, self()}; - (A, B) -> meck:passthrough([A, B]) - end), - mongoose_listener_sup:start_link(), - %% HTTP API config - Opts = #{transport => #{num_acceptors => 10, max_connections => 1024}, - protocol => #{}, - port => ?PORT, - ip_tuple => ?IP, - proto => tcp, - handlers => [#{host => "localhost", path => "/api", module => Module}]}, - ejabberd_cowboy:start_listener(Opts). - -teardown() -> - cowboy:stop_listener(ejabberd_cowboy:ref({?PORT, ?IP, tcp})), - mongoose_commands:unregister(commands_new()), - meck:unload(ejabberd_auth), - meck:unload(gen_hook), - meck:unload(supervisor), - mc_holder_proc ! stop, - ok. - -init_per_suite(C) -> - application:ensure_all_started(cowboy), - application:ensure_all_started(jid), - application:ensure_all_started(fusco), - ok = mnesia:start(), - C. - -end_per_suite(C) -> - stopped = mnesia:stop(), - mnesia:delete_schema([node()]), - application:stop(fusco), - application:stop(cowboy), - C. - -init_per_group(_, C) -> - C. - -end_per_group(_, C) -> - C. - -init_per_testcase(_, C) -> - C. - -end_per_testcase(_, C) -> - C. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% Backend side tests -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -get_simple(_Config) -> - Arg = {arg1, <<"bob@localhost">>}, - Base = "/api/users", - ExpectedBody = get_simple_command(element(2, Arg)), - {ok, Response} = request(create_path_with_binds(Base, [Arg]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -delete_simple(_Config) -> - Arg1 = {arg1, <<"ala_ma_kota">>}, - Arg2 = {arg2, 2}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "DELETE", admin), - check_status_code(Response, 204). - -post_simple(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg1, Arg2], - Path = <<"/api/weather">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix()++"/api/weather/" ++ Result)). - -post_simple_with_subcategory(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg2], - Path = <<"/api/weather/10/subcategory">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix()++"/api/weather/10/subcategory/" ++ Result)). - -put_simple(_Config) -> - Binds = [{arg1, <<"username">>}, {arg2,<<"localhost">>}], - Args = [{arg3, <<"newusername">>}], - Base = "/api/users", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Args, admin), - check_status_code(Response, 204). - -get_two_args(_Config) -> - Arg1 = {arg1, 1}, - Arg2 = {arg2, 2}, - Base = "/api/animals", - ExpectedBody = get_two_args_command(element(2, Arg1), element(2, Arg2)), - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_two_args_different_types(_Config) -> - Arg1 = {one, 1}, - Arg2 = {two, <<"mybin">>}, - Base = "/api/books", - ExpectedBody = get_two_args2_command(element(2, Arg1), element(2, Arg2)), - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_wrong_path(_Config) -> - Path = <<"/api/animals2/1/2">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_wrong_arg_number(_Config) -> - Path = <<"/api/animals/1/2/3">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_no_command(_Config) -> - Path = <<"/api/unregistered_command/123123">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_wrong_arg_type(_Config) -> - Path = <<"/api/animals/1/wrong">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 400). - -post_wrong_arg_number(_Config) -> - Args = [{arg1, 10}, {arg2,2}, {arg3, 100}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 404). - -post_wrong_arg_name(_Config) -> - Args = [{arg11, 10}, {arg2,2}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 400). - -post_wrong_arg_type(_Config) -> - Args = [{arg1, 10}, {arg2,<<"weird binary">>}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 400). - -post_different_arg_order(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg2, Arg1], - Path = <<"/api/weather">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix() ++"/api/weather/" ++ Result)). - -post_no_command(_Config) -> - Args = [{arg1, 10}, {arg2,2}], - Path = <<"/api/weather/10">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 404). - - -delete_wrong_arg_order(_Config) -> - Arg1 = {arg1, <<"ala_ma_kota">>}, - Arg2 = {arg2, 2}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg2, Arg1]), "DELETE", admin), - check_status_code(Response, 400). - -delete_wrong_arg_types(_Config) -> - Arg1 = {arg1, 2}, - Arg2 = {arg2, <<"ala_ma_kota">>}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "DELETE", admin), - check_status_code(Response, 400). - -put_wrong_param_type(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{age, <<"23">>}, {kids, 10}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_wrong_bind_type(_Config) -> - Binds = [{username, <<"username">>}, {domain, 123}], - Parameters = [{age, 23}, {kids, 10}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_different_params_order(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{kids, 2}, {age, 45}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 200). - -put_wrong_binds_order(_Config) -> - Binds = [{domain, <<"domain">>}, {username, <<"username">>}], - Parameters = [{kids, 2}, {age, 30}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_too_less_params(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_too_less_binds(_Config) -> - Binds = [{username, <<"username">>}], - Parameters = [{age, 20}, {kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -put_wrong_bind_name(_Config) -> - Binds = [{usersrejm, <<"username">>}, {domain, <<"localhost">>}], - Parameters = [{age, 20}, {kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -put_wrong_param_name(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"localhost">>}], - Parameters = [{age, 20}, {srids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% definitions -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -commands_admin() -> - [ - [ - {name, get_simple}, - {category, <<"users">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_simple_command}, - {action, read}, - {identifiers, []}, - {args, [{arg1, binary}]}, - {result, {result, binary}} - ], - [ - {name, get_advanced}, - {category, <<"animals">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_two_args_command}, - {action, read}, - {identifiers, []}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, get_advanced2}, - {category, <<"books">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_two_args2_command}, - {action, read}, - {identifiers, []}, - {args, [{one, integer}, {two, binary}]}, - {result, {result, integer}} - ], - [ - {name, post_simple}, - {category, <<"weather">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, post_simple_command}, - {action, create}, - {identifiers, []}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, post_simple2}, - {category, <<"weather">>}, - {subcategory, <<"subcategory">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, post_simple_command}, - {action, create}, - {identifiers, [arg1]}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, delete_simple}, - {category, <<"music">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, delete_simple_command}, - {action, delete}, - {identifiers, []}, - {args, [{arg1, binary}, {arg2, integer}]}, - {result, ok} - ], - [ - {name, put_simple}, - {category, <<"users">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, put_simple_command}, - {action, update}, - {args, [{arg1, binary}, {arg2, binary}, {arg3, binary}]}, - {identifiers, [arg1, arg2]}, - {result, ok} - ], - [ - {name, put_advanced}, - {category, <<"dragons">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, put_advanced_command}, - {action, update}, - {args, [{username, binary}, - {domain, binary}, - {age, integer}, - {kids, integer}]}, - {identifiers, [username, domain]}, - {result, ok} - ] - ]. - -commands_new() -> - commands_admin(). - -%% admin command funs -get_simple_command(<<"bob@localhost">>) -> - <<"bob is OK">>. - -get_two_args_command(1, 2) -> - <<"all is working">>. - -get_two_args2_command(X, B) when is_integer(X) and is_binary(B) -> - 100. - -post_simple_command(_X, 2) -> - <<"new_resource">>. - -delete_simple_command(Binary, 2) when is_binary(Binary) -> - 10. - -put_simple_command(_Arg1, _Arg2, _Arg3) -> - ok. - -put_advanced_command(Arg1, Arg2, Arg3, Arg4) when is_binary(Arg1) and is_binary(Arg2) - and is_integer(Arg3) and is_integer(Arg4) -> - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% utilities -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -build_path_prefix() -> - "http://" ++ ?HOST ++ ":" ++ integer_to_list(?PORT). - -maybe_add_body([]) -> - []; -maybe_add_body(Args) -> - jiffy:encode(maps:from_list(Args)). - -maybe_add_accepted_headers("POST") -> - accepted_headers(); -maybe_add_accepted_headers("PUT") -> - accepted_headers(); -maybe_add_accepted_headers(_) -> - []. - -accepted_headers() -> - [{<<"Content-Type">>, <<"application/json">>}, {<<"Accept">>, <<"application/json">>}]. - -maybe_add_auth_header(admin) -> - []. - --spec create_path_with_binds(string(), list()) -> binary(). -create_path_with_binds(Base, ArgList) when is_list(ArgList) -> - list_to_binary( - lists:flatten(Base ++ ["/" ++ to_list(ArgValue) - || {ArgName, ArgValue} <- ArgList])). - -to_list(Int) when is_integer(Int) -> - integer_to_list(Int); -to_list(Float) when is_float(Float) -> - float_to_list(Float); -to_list(Bin) when is_binary(Bin) -> - binary_to_list(Bin); -to_list(Atom) when is_atom(Atom) -> - atom_to_list(Atom); -to_list(Other) -> - Other. - --spec request(binary(), method(), admin | {{binary(), binary()}, boolean()}) -> any. -request(Path, "GET", Entity) -> - request(Path, "GET", [], Entity); -request(Path, "DELETE", Entity) -> - request(Path, "DELETE", [], Entity). - --spec request(binary(), method(), list({atom(), any()}), - {headers, list()} | admin | {{binary(), binary()}, boolean()}) -> any. -do_request(Path, Method, Body, {headers, Headers}) -> - {ok, Pid} = fusco:start_link("http://"++ ?HOST ++ ":" ++ integer_to_list(?PORT), []), - R = fusco:request(Pid, Path, Method, Headers, Body, 5000), - fusco:disconnect(Pid), - teardown(), - R. - -request(Path, Method, BodyData, admin) -> - ct:pal("~p, ~p, ~p", [Path, Method, BodyData]), - setup(backend_module()), - Body = maybe_add_body(BodyData), - AcceptHeader = maybe_add_accepted_headers(Method), - do_request(Path, Method, Body, {headers, AcceptHeader}). - -mc_holder() -> - erlang:register(mc_holder_proc, self()), - mongoose_commands:init(), - mongoose_commands:register(commands_new()), - receive - _ -> ok - end, - erlang:unregister(mc_holder_proc). - -check_status_code(Response, Code) when is_integer(Code) -> - {{ResCode, _}, _, _, _, _} = Response, - ?assertEqual(Code, binary_to_integer(ResCode)); -check_status_code(_R, Code) -> - ?assertEqual(Code, not_a_number). - -check_response_body(Response, ExpectedBody) -> - {_, _, Body, _ , _} = Response, - ?assertEqual(binary_to_list(Body), "\"" ++ binary_to_list(ExpectedBody) ++ "\""). - -check_location_header(Response, Path) -> - {_, Headers, _, _ , _} = Response, - Location = proplists:get_value(<<"location">>, Headers), - ?assertEqual(Path, Location). diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 1c47c933664..ef9c7ad92a8 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -170,7 +170,7 @@ options("mongooseim-pgsql") -> config([listen, http, handlers, mod_websockets], #{host => '_', path => "/ws-xmpp", max_stanza_size => 100, ping_rate => 120000, timeout => infinity}), - config([listen, http, handlers, mongoose_api_admin], + config([listen, http, handlers, mongoose_admin_api], #{host => "localhost", path => "/api", username => <<"ala">>, password => <<"makotaipsa">>}) ], @@ -183,7 +183,7 @@ options("mongooseim-pgsql") -> port => 8088, transport => #{num_acceptors => 10, max_connections => 1024}, handlers => - [config([listen, http, handlers, mongoose_api_admin], + [config([listen, http, handlers, mongoose_admin_api], #{host => "localhost", path => "/api"})] }), config([listen, http], @@ -680,10 +680,9 @@ pgsql_modules() -> #{mod_adhoc => default_mod_config(mod_adhoc), mod_amp => #{}, mod_blocking => default_mod_config(mod_blocking), mod_bosh => default_mod_config(mod_bosh), - mod_carboncopy => default_mod_config(mod_carboncopy), mod_commands => #{}, + mod_carboncopy => default_mod_config(mod_carboncopy), mod_disco => mod_config(mod_disco, #{users_can_see_hidden_services => false}), mod_last => mod_config(mod_last, #{backend => rdbms}), - mod_muc_commands => #{}, mod_muc_light_commands => #{}, mod_offline => mod_config(mod_offline, #{backend => rdbms}), mod_privacy => mod_config(mod_privacy, #{backend => rdbms}), mod_private => default_mod_config(mod_private), diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index e97d7e0b7ee..d65ca2832d8 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -97,8 +97,8 @@ groups() -> listen_http_handlers_bosh, listen_http_handlers_websockets, listen_http_handlers_client_api, + listen_http_handlers_admin_api, listen_http_handlers_api, - listen_http_handlers_api_admin, listen_http_handlers_domain, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, @@ -603,16 +603,16 @@ listen_http_handlers_client_api(_Config) -> ?err(T(#{<<"handlers">> => [not_a_module]})), ?err(T(#{<<"docs">> => <<"maybe">>})). +listen_http_handlers_admin_api(_Config) -> + {P, T} = test_listen_http_handler(mongoose_admin_api), + test_listen_http_handler_creds(P, T). + listen_http_handlers_api(_Config) -> {P, T} = test_listen_http_handler(mongoose_api), ?cfg(P ++ [handlers], [mongoose_api_metrics], T(#{<<"handlers">> => [<<"mongoose_api_metrics">>]})), ?err(T(#{<<"handlers">> => [not_a_module]})). -listen_http_handlers_api_admin(_Config) -> - {P, T} = test_listen_http_handler(mongoose_api_admin), - test_listen_http_handler_creds(P, T). - listen_http_handlers_domain(_Config) -> {P, T} = test_listen_http_handler(mongoose_domain_handler), test_listen_http_handler_creds(P, T). diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index 301b3d01c2d..3f4f4e8e475 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -38,7 +38,7 @@ tls.keyfile = "priv/dc1.pem" tls.password = "" - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" username = "ala" @@ -61,7 +61,7 @@ transport.num_acceptors = 10 transport.max_connections = 1024 - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" @@ -176,12 +176,6 @@ [modules.mod_disco] users_can_see_hidden_services = false -[modules.mod_commands] - -[modules.mod_muc_commands] - -[modules.mod_muc_light_commands] - [modules.mod_last] backend = "rdbms" diff --git a/test/mongoose_api_common_SUITE.erl b/test/mongoose_api_common_SUITE.erl deleted file mode 100644 index eb47778091c..00000000000 --- a/test/mongoose_api_common_SUITE.erl +++ /dev/null @@ -1,72 +0,0 @@ --module(mongoose_api_common_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("eunit/include/eunit.hrl"). - - --define(aq(E, V), ( - [ct:fail("ASSERT EQUAL~n\tExpected ~p~n\tActual ~p~n", [(E), (V)]) - || (E) =/= (V)] - )). - -all() -> - [url_is_correct_for_create_command, - url_is_correct_for_read_command, - url_is_correct_for_read_command_with_subcategory]. - -url_is_correct_for_create_command(_) -> - Cmd = create_cmd(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host">>, Url). - -url_is_correct_for_read_command(_) -> - Cmd = read_cmd(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host">>, Url). - -url_is_correct_for_read_command_with_subcategory(_) -> - Cmd = read_cmd2(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host/rosters">>, Url). - -create_cmd() -> - Props = [ - {name, registeruser}, - {category, <<"users">>}, - {desc, <<"Register a user">>}, - {module, ?MODULE}, - {function, register}, - {action, create}, - {args, [{user, binary}, {host, binary}, {password, binary}]}, - {identifiers, [host]}, - {result, {msg, binary}} - ], - mongoose_commands:new(Props). - -read_cmd() -> - Props = [ - {name, listusers}, - {category, <<"users">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - mongoose_commands:new(Props). - -read_cmd2() -> - Props = [ - {name, listusers}, - {category, <<"users">>}, - {subcategory, <<"rosters">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - mongoose_commands:new(Props). -