aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>2016-11-12 13:27:15 +0300
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>2016-11-12 13:27:15 +0300
commit78a44e01762e00102f5e3e3f0b49690cc7866c31 (patch)
treeb8ac7773f510ee3c1da4802bce2badc71c34c0b2 /src
parentAdd more tests for offline storage (diff)
parentSupport several groups separated by ; in add_rosteritem command (diff)
Merge branch 'master' into xml-ng
Conflicts: src/adhoc.erl src/cyrsasl_oauth.erl src/ejabberd_c2s.erl src/ejabberd_config.erl src/ejabberd_service.erl src/gen_mod.erl src/mod_admin_extra.erl src/mod_announce.erl src/mod_carboncopy.erl src/mod_client_state.erl src/mod_configure.erl src/mod_echo.erl src/mod_mam.erl src/mod_muc.erl src/mod_muc_room.erl src/mod_offline.erl src/mod_pubsub.erl src/mod_stats.erl src/node_flat_sql.erl src/randoms.erl
Diffstat (limited to 'src')
-rw-r--r--src/acl.erl62
-rw-r--r--src/cyrsasl_oauth.erl2
-rw-r--r--src/cyrsasl_scram.erl4
-rw-r--r--src/ejabberd.erl4
-rw-r--r--src/ejabberd_access_permissions.erl543
-rw-r--r--src/ejabberd_admin.erl16
-rw-r--r--src/ejabberd_app.erl28
-rw-r--r--src/ejabberd_auth_mnesia.erl2
-rw-r--r--src/ejabberd_auth_riak.erl2
-rw-r--r--src/ejabberd_auth_sql.erl2
-rw-r--r--src/ejabberd_c2s.erl201
-rw-r--r--src/ejabberd_commands.erl352
-rw-r--r--src/ejabberd_config.erl71
-rw-r--r--src/ejabberd_ctl.erl23
-rw-r--r--src/ejabberd_http.erl17
-rw-r--r--src/ejabberd_http_bind.erl3
-rw-r--r--src/ejabberd_http_ws.erl3
-rw-r--r--src/ejabberd_local.erl25
-rw-r--r--src/ejabberd_oauth.erl371
-rw-r--r--src/ejabberd_oauth_mnesia.erl65
-rw-r--r--src/ejabberd_oauth_rest.erl98
-rw-r--r--src/ejabberd_oauth_sql.erl78
-rw-r--r--src/ejabberd_s2s.erl26
-rw-r--r--src/ejabberd_s2s_out.erl3
-rw-r--r--src/ejabberd_service.erl94
-rw-r--r--src/ejabberd_sm.erl54
-rw-r--r--src/ejabberd_sm_redis.erl7
-rw-r--r--src/ejabberd_sql.erl8
-rw-r--r--src/ejabberd_web_admin.erl54
-rw-r--r--src/ejabberd_xmlrpc.erl80
-rw-r--r--src/ext_mod.erl37
-rw-r--r--src/extauth.erl3
-rw-r--r--src/gen_mod.erl63
-rw-r--r--src/http_p1.erl358
-rw-r--r--src/jid.erl26
-rw-r--r--src/jlib.erl36
-rw-r--r--src/mod_admin_extra.erl72
-rw-r--r--src/mod_announce.erl14
-rw-r--r--src/mod_carboncopy.erl6
-rw-r--r--src/mod_client_state.erl50
-rw-r--r--src/mod_configure.erl36
-rw-r--r--src/mod_delegation.erl325
-rw-r--r--src/mod_echo.erl2
-rw-r--r--src/mod_http_api.erl319
-rw-r--r--src/mod_http_upload_quota.erl6
-rw-r--r--src/mod_irc.erl2
-rw-r--r--src/mod_mam.erl109
-rw-r--r--src/mod_mix.erl2
-rw-r--r--src/mod_muc.erl119
-rw-r--r--src/mod_muc_admin.erl147
-rw-r--r--src/mod_muc_log.erl2
-rw-r--r--src/mod_muc_room.erl429
-rw-r--r--src/mod_offline.erl28
-rw-r--r--src/mod_offline_sql.erl2
-rw-r--r--src/mod_privacy_sql.erl2
-rw-r--r--src/mod_privilege.erl348
-rw-r--r--src/mod_pubsub.erl2
-rw-r--r--src/mod_roster.erl92
-rw-r--r--src/mod_stats.erl9
-rw-r--r--src/node_flat_sql.erl11
-rw-r--r--src/node_mb.erl3
-rw-r--r--src/node_mb_sql.erl158
-rw-r--r--src/nodetree_tree_sql.erl6
-rw-r--r--src/randoms.erl21
-rw-r--r--src/rest.erl181
-rw-r--r--src/xmpp_codec.erl547
-rw-r--r--src/xmpp_util.erl14
67 files changed, 4687 insertions, 1198 deletions
diff --git a/src/acl.erl b/src/acl.erl
index 7519e12e2..e3fdfcae1 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -31,11 +31,13 @@
-export([add_access/3, clear/0]).
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
- load_from_config/0, match_rule/3,
+ load_from_config/0, match_rule/3, any_rules_allowed/3,
transform_options/1, opt_type/1, acl_rule_matches/3,
acl_rule_verify/1, access_matches/3,
transform_access_rules_config/1,
- access_rules_validator/1, shaper_rules_validator/1]).
+ parse_ip_netmask/1,
+ access_rules_validator/1, shaper_rules_validator/1,
+ normalize_spec/1, resolve_access/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -74,12 +76,6 @@
-export_type([acl/0]).
start() ->
- case catch mnesia:table_info(acl, storage_type) of
- disc_copies ->
- mnesia:delete_table(acl);
- _ ->
- ok
- end,
mnesia:create_table(acl,
[{ram_copies, [node()]}, {type, bag},
{local_content, true},
@@ -261,6 +257,7 @@ normalize_spec(Spec) ->
{server, S} -> {server, nameprep(S)};
{resource, R} -> {resource, resourceprep(R)};
{server_regexp, SR} -> {server_regexp, b(SR)};
+ {resource_regexp, R} -> {resource_regexp, b(R)};
{server_glob, S} -> {server_glob, b(S)};
{resource_glob, R} -> {resource_glob, b(R)};
{ip, {Net, Mask}} -> {ip, {Net, Mask}};
@@ -274,6 +271,15 @@ normalize_spec(Spec) ->
end
end.
+-spec any_rules_allowed(global | binary(), access_name(),
+ jid() | ljid() | inet:ip_address()) -> boolean().
+
+any_rules_allowed(Host, Access, Entity) ->
+ lists:any(fun (Rule) ->
+ allow == acl:match_rule(Host, Rule, Entity)
+ end,
+ Access).
+
-spec match_rule(global | binary(), access_name(),
jid() | ljid() | inet:ip_address()) -> any().
@@ -432,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
acl_rule_matches(_ACL, _Data, _Host) ->
false.
--spec access_matches(atom()|list(), any(), global|binary()) -> any().
-access_matches(all, _Data, _Host) ->
- allow;
-access_matches(none, _Data, _Host) ->
- deny;
-access_matches(Name, Data, Host) when is_atom(Name) ->
- GAccess = ets:lookup(access, {Name, global}),
+resolve_access(all, _Host) ->
+ all;
+resolve_access(none, _Host) ->
+ none;
+resolve_access(Name, Host) when is_atom(Name) ->
+ GAccess = mnesia:dirty_read(access, {Name, global}),
LAccess =
- if Host /= global -> ets:lookup(access, {Name, Host});
+ if Host /= global -> mnesia:dirty_read(access, {Name, Host});
true -> []
end,
case GAccess ++ LAccess of
[] ->
- deny;
+ [];
AccessList ->
- Rules = lists:flatmap(
+ lists:flatmap(
fun(#access{rules = Rs}) ->
Rs
- end, AccessList),
- access_rules_matches(Rules, Data, Host)
+ end, AccessList)
end;
-access_matches(Rules, Data, Host) when is_list(Rules) ->
- access_rules_matches(Rules, Data, Host).
-
+resolve_access(Rules, _Host) when is_list(Rules) ->
+ Rules.
+
+-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
+access_matches(Rules, Data, Host) ->
+ case resolve_access(Rules, Host) of
+ all -> allow;
+ none -> deny;
+ RRules -> access_rules_matches(RRules, Data, Host)
+ end.
-spec access_rules_matches(list(), any(), global|binary()) -> any().
@@ -473,7 +484,7 @@ access_rules_matches([], _Data, _Host, Default) ->
Default.
get_aclspecs(ACL, Host) ->
- ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}).
+ mnesia:dirty_read(acl, {ACL, Host}) ++ mnesia:dirty_read(acl, {ACL, global}).
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
@@ -676,7 +687,8 @@ transform_options({acl, Name, Type}, Opts) ->
{server_regexp, SR} -> {server_regexp, [b(SR)]};
{server_glob, S} -> {server_glob, [b(S)]};
{ip, S} -> {ip, [b(S)]};
- {resource_glob, R} -> {resource_glob, [b(R)]}
+ {resource_glob, R} -> {resource_glob, [b(R)]};
+ {resource_regexp, R} -> {resource_regexp, [b(R)]}
end,
[{acl, [{Name, [T]}]}|Opts];
transform_options({access, Name, Rules}, Opts) ->
diff --git a/src/cyrsasl_oauth.erl b/src/cyrsasl_oauth.erl
index 09d143ef5..21dedc6db 100644
--- a/src/cyrsasl_oauth.erl
+++ b/src/cyrsasl_oauth.erl
@@ -51,7 +51,7 @@ mech_step(State, ClientIn) ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]};
- false ->
+ _ ->
{error, 'not-authorized', User}
end;
_ -> {error, 'bad-protocol'}
diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl
index fdc40cd86..1e2a5c681 100644
--- a/src/cyrsasl_scram.erl
+++ b/src/cyrsasl_scram.erl
@@ -85,7 +85,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
if is_tuple(Ret) -> Ret;
true ->
TempSalt =
- crypto:rand_bytes(?SALT_LENGTH),
+ randoms:bytes(?SALT_LENGTH),
SaltedPassword =
scram:salted_password(Ret,
TempSalt,
@@ -99,7 +99,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
str:substr(ClientIn,
str:str(ClientIn, <<"n=">>)),
ServerNonce =
- jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)),
+ jlib:encode_base64(randoms:bytes(?NONCE_LENGTH)),
ServerFirstMessage =
iolist_to_binary(
["r=",
diff --git a/src/ejabberd.erl b/src/ejabberd.erl
index 6bd2422ae..5a6fc64d7 100644
--- a/src/ejabberd.erl
+++ b/src/ejabberd.erl
@@ -105,8 +105,6 @@ start_app([], _Type, _StartFlag) ->
ok.
check_app_modules(App, StartFlag) ->
- {A, B, C} = p1_time_compat:timestamp(),
- random:seed(A, B, C),
sleep(5000),
case application:get_key(App, modules) of
{ok, Mods} ->
@@ -140,7 +138,7 @@ exit_or_halt(Reason, StartFlag) ->
end.
sleep(N) ->
- timer:sleep(random:uniform(N)).
+ timer:sleep(randoms:uniform(N)).
get_module_file(App, Mod) ->
BaseName = atom_to_list(Mod),
diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl
new file mode 100644
index 000000000..7ce75aa9c
--- /dev/null
+++ b/src/ejabberd_access_permissions.erl
@@ -0,0 +1,543 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_access_permissions.erl
+%%% Author : Paweł Chmielowski <pawel@process-one.net>
+%%% Purpose : Administrative functions and commands
+%%% Created : 7 Sep 2016 by Paweł Chmielowski <pawel@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%-------------------------------------------------------------------
+-module(ejabberd_access_permissions).
+-author("pawel@process-one.net").
+
+-include("ejabberd_commands.hrl").
+-include("logger.hrl").
+
+-behaviour(gen_server).
+-behavior(ejabberd_config).
+
+%% API
+-export([start_link/0,
+ parse_api_permissions/1,
+ can_access/2,
+ invalidate/0,
+ opt_type/1,
+ show_current_definitions/0,
+ register_permission_addon/2,
+ unregister_permission_addon/1]).
+
+%% gen_server callbacks
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {
+ definitions = none,
+ fragments_generators = []
+}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+-spec can_access(atom(), map()) -> allow | deny.
+can_access(Cmd, CallerInfo) ->
+ gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
+
+-spec invalidate() -> ok.
+invalidate() ->
+ gen_server:cast(?MODULE, invalidate).
+
+-spec register_permission_addon(atom(), fun()) -> ok.
+register_permission_addon(Name, Fun) ->
+ gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}).
+
+-spec unregister_permission_addon(atom()) -> ok.
+unregister_permission_addon(Name) ->
+ gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}).
+
+-spec show_current_definitions() -> any().
+show_current_definitions() ->
+ gen_server:call(?MODULE, show_current_definitions).
+
+%%--------------------------------------------------------------------
+%% @doc
+%% Starts the server
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
+start_link() ->
+ gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Initializes the server
+%%
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @end
+%%--------------------------------------------------------------------
+-spec init(Args :: term()) ->
+ {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term()} | ignore.
+init([]) ->
+ {ok, #state{}}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling call messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()},
+ State :: #state{}) ->
+ {reply, Reply :: term(), NewState :: #state{}} |
+ {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
+ CallerModule = maps:get(caller_module, CallerInfo, none),
+ Host = maps:get(caller_host, CallerInfo, global),
+ {State2, Defs0} = get_definitions(State),
+ Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0,
+ Res = lists:foldl(
+ fun({Name, _} = Def, none) ->
+ case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of
+ true ->
+ ?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
+ allow;
+ _ ->
+ none
+ end;
+ (_, Val) ->
+ Val
+ end, none, Defs),
+ Res2 = case Res of
+ allow -> allow;
+ _ ->
+ ?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]),
+ deny
+ end,
+ {reply, Res2, State2};
+handle_call(show_current_definitions, _From, State) ->
+ {State2, Defs} = get_definitions(State),
+ {reply, Defs, State2};
+handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keystore(Name, 1, Gens, {Name, Fun}),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keydelete(Name, 1, Gens),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling cast messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_cast(Request :: term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_cast(invalidate, State) ->
+ {noreply, State#state{definitions = none}};
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling all non call/cast messages
+%%
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_info(Info :: timeout() | term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any
+%% necessary cleaning up. When it returns, the gen_server terminates
+%% with Reason. The return value is ignored.
+%%
+%% @spec terminate(Reason, State) -> void()
+%% @end
+%%--------------------------------------------------------------------
+-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
+ State :: #state{}) -> term().
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Convert process state when code is changed
+%%
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @end
+%%--------------------------------------------------------------------
+-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{},
+ Extra :: term()) ->
+ {ok, NewState :: #state{}} | {error, Reason :: term()}.
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+-spec get_definitions(#state{}) -> {#state{}, any()}.
+get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) ->
+ DefaultOptions = [{<<"console commands">>,
+ {[ejabberd_ctl],
+ [{acl, all}],
+ {all, none}}},
+ {<<"admin access">>,
+ {[],
+ [{acl, admin}],
+ {all, [start, stop]}}}],
+ NDefs = case Defs of
+ none ->
+ ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions),
+ AllCommands = ejabberd_commands:get_commands_definition(),
+ Frags = lists:foldl(
+ fun({_Name, Generator}, Acc) ->
+ Acc ++ Generator()
+ end, [], Gens),
+ lists:map(
+ fun({Name, {From, Who, {Add, Del}}}) ->
+ Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
+ {Name, {From, Who, Cmds}}
+ end, ApiPerms ++ Frags);
+ V ->
+ V
+ end,
+ {State#state{definitions = NDefs}, NDefs}.
+
+matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) ->
+ case What == all orelse lists:member(Cmd, What) of
+ true ->
+ case From == [] orelse lists:member(Module, From) of
+ true ->
+ Scope = maps:get(oauth_scope, CallerInfo, none),
+ lists:any(
+ fun({access, Access}) when Scope == none ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) when Scope == none ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host);
+ ({oauth, Scopes, List}) when Scope /= none ->
+ case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
+ true ->
+ lists:any(
+ fun({access, Access}) ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host)
+ end, List);
+ _ ->
+ false
+ end;
+ (_) ->
+ false
+ end, Who);
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+filter_commands_with_permissions(AllCommands, Add, Del) ->
+ CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
+ CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []),
+ lists:map(fun(#ejabberd_commands{name = N}) -> N end,
+ CommandsAdd -- CommandsDel).
+
+filter_commands_with_patterns([], _Patterns, Acc) ->
+ Acc;
+filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
+ case command_matches_patterns(C, Patterns) of
+ true ->
+ filter_commands_with_patterns(CRest, Patterns, [C | Acc]);
+ _ ->
+ filter_commands_with_patterns(CRest, Patterns, Acc)
+ end.
+
+command_matches_patterns(_, all) ->
+ true;
+command_matches_patterns(_, none) ->
+ false;
+command_matches_patterns(_, []) ->
+ false;
+command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) ->
+ case lists:member(Tag, Tags) of
+ true ->
+ true;
+ _ ->
+ command_matches_patterns(C, Tail)
+ end;
+command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
+ true;
+command_matches_patterns(C, [_ | Tail]) ->
+ command_matches_patterns(C, Tail).
+
+%%%===================================================================
+%%% Options parsing code
+%%%===================================================================
+
+parse_api_permissions(Data) when is_list(Data) ->
+ throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}).
+
+parse_api_permission(Name, Args) ->
+ {From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of
+ {error, Msg} ->
+ report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]);
+ Val -> Val
+ end,
+ {Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}.
+
+parse_from(_Name, Module) when is_atom(Module) ->
+ [Module];
+parse_from(Name, Modules) when is_list(Modules) ->
+ lists:foreach(fun(Module) when is_atom(Module) ->
+ ok;
+ (Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name])
+ end, Modules),
+ Modules;
+parse_from(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_who(Name, Atom, ParseOauth) when is_atom(Atom) ->
+ parse_who(Name, [Atom], ParseOauth);
+parse_who(Name, Defs, ParseOauth) when is_list(Defs) ->
+ lists:map(
+ fun([{access, Val}]) ->
+ try acl:access_rules_validator(Val) of
+ Rule ->
+ {access, Rule}
+ catch
+ throw:{invalid_syntax, Msg} ->
+ report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>,
+ [Msg, Name]);
+ throw:{replace_with, NVal} ->
+ {access, NVal};
+ error:_ ->
+ report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name])
+ end;
+ ([{oauth, OauthList}]) when is_list(OauthList) ->
+ case ParseOauth of
+ oauth ->
+ Nested = parse_who(Name, lists:flatten(OauthList), scope),
+ {Scopes, Rest} = lists:partition(
+ fun({scope, _}) -> true;
+ (_) -> false
+ end, Nested),
+ case Scopes of
+ [] ->
+ report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>,
+ [Name]);
+ _ ->
+ {oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest}
+ end;
+ scope ->
+ report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>,
+ [Name])
+ end;
+ ({scope, ScopeList}) ->
+ case ParseOauth of
+ oauth ->
+ report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>,
+ [Name]);
+ scope ->
+ ScopeList2 = case ScopeList of
+ V when is_binary(V) -> [V];
+ V2 when is_list(V2) -> V2;
+ V3 ->
+ report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>,
+ [V3, Name])
+ end,
+ {scope, ScopeList2}
+ end;
+ (Atom) when is_atom(Atom) ->
+ {acl, Atom};
+ ([Other]) ->
+ try acl:normalize_spec(Other) of
+ Rule2 ->
+ {acl, Rule2}
+ catch
+ _:_ ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Other, Name])
+ end;
+ (Invalid) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Invalid, Name])
+ end, Defs);
+parse_who(Name, Val, _ParseOauth) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_what(Name, Binary) when is_binary(Binary) ->
+ parse_what(Name, [Binary]);
+parse_what(Name, Defs) when is_list(Defs) ->
+ {A, D} = lists:foldl(
+ fun(Def, {Add, Del}) ->
+ case parse_single_what(Def) of
+ {error, Err} ->
+ report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>,
+ [Err, Def, Name]);
+ all ->
+ {case Add of none -> none; _ -> all end, Del};
+ {neg, all} ->
+ {none, all};
+ {neg, Value} ->
+ {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end};
+ Value ->
+ {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
+ end
+ end, {[], []}, Defs),
+ case {A, D} of
+ {[], _} ->
+ {none, all};
+ {A2, []} ->
+ {A2, none};
+ V ->
+ V
+ end;
+parse_what(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_single_what(<<"*">>) ->
+ all;
+parse_single_what(<<"!*">>) ->
+ {neg, all};
+parse_single_what(<<"!", Rest/binary>>) ->
+ case parse_single_what(Rest) of
+ {neg, _} ->
+ {error, <<"Double negation">>};
+ {error, _} = Err ->
+ Err;
+ V ->
+ {neg, V}
+ end;
+parse_single_what(<<"[tag:", Rest/binary>>) ->
+ case binary:split(Rest, <<"]">>) of
+ [TagName, <<"">>] ->
+ case parse_single_what(TagName) of
+ {error, _} = Err ->
+ Err;
+ V when is_atom(V) ->
+ {tag, V};
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+parse_single_what(Binary) when is_binary(Binary) ->
+ case is_valid_command_name(Binary) of
+ true ->
+ binary_to_atom(Binary, latin1);
+ _ ->
+ {error, <<"Invalid value">>}
+ end;
+parse_single_what(_) ->
+ {error, <<"Invalid value">>}.
+
+is_valid_command_name(<<>>) ->
+ false;
+is_valid_command_name(Val) ->
+ is_valid_command_name2(Val).
+
+is_valid_command_name2(<<>>) ->
+ true;
+is_valid_command_name2(<<K:8, Rest/binary>>) when K >= $a andalso K =< $z orelse K == $_ ->
+ is_valid_command_name2(Rest);
+is_valid_command_name2(_) ->
+ false.
+
+key_split(Args, Fields) ->
+ {_, Order1, Results1, Required1} = lists:foldl(
+ fun({Field, Default}, {Idx, Order, Results, Required}) ->
+ {Idx + 1, maps:put(Field, Idx, Order), [Default | Results], Required};
+ (Field, {Idx, Order, Results, Required}) ->
+ {Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)}
+ end, {1, #{}, [], #{}}, Fields),
+ key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}).
+
+key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 ->
+ parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]);
+key_split([], Results, _Order, _Required, _Duplicates) ->
+ Results;
+key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) ->
+ case maps:find(Arg, Order) of
+ {ok, Idx} ->
+ case maps:is_key(Arg, Duplicates) of
+ false ->
+ Results2 = setelement(Idx, Results, Value),
+ key_split(Rest, Results2, Order, maps:remove(Arg, Required), maps:put(Arg, 1, Duplicates));
+ true ->
+ parse_error(<<"Duplicate field '~s'">>, [Arg])
+ end;
+ _ ->
+ parse_error(<<"Unknown field '~s'">>, [Arg])
+ end.
+
+report_error(Format, Args) ->
+ throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}).
+
+parse_error(Format, Args) ->
+ {error, iolist_to_binary(io_lib:format(Format, Args))}.
+
+opt_type(api_permissions) ->
+ fun parse_api_permissions/1;
+opt_type(_) ->
+ [api_permissions].
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 87ac76875..8622ea8d0 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -87,6 +87,7 @@ get_commands_spec() ->
args = [], result = {res, rescode}},
#ejabberd_commands{name = reopen_log, tags = [logs, server],
desc = "Reopen the log files",
+ policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
#ejabberd_commands{name = rotate_log, tags = [logs, server],
@@ -129,6 +130,7 @@ get_commands_spec() ->
#ejabberd_commands{name = register, tags = [accounts],
desc = "Register a user",
+ policy = admin,
module = ?MODULE, function = register,
args = [{user, binary}, {host, binary}, {password, binary}],
result = {res, restuple}},
@@ -166,7 +168,7 @@ get_commands_spec() ->
#ejabberd_commands{name = list_cluster, tags = [cluster],
desc = "List nodes that are part of the cluster handled by Node",
module = ?MODULE, function = list_cluster,
- args = [],
+ args = [],
result = {nodes, {list, {node, atom}}}},
#ejabberd_commands{name = import_file, tags = [mnesia],
@@ -220,7 +222,7 @@ get_commands_spec() ->
desc = "Delete offline messages older than DAYS",
module = ?MODULE, function = delete_old_messages,
args = [{days, integer}], result = {res, rescode}},
-
+
#ejabberd_commands{name = export2sql, tags = [mnesia],
desc = "Export virtual host information from Mnesia tables to SQL files",
module = ejd2sql, function = export,
@@ -378,13 +380,12 @@ register(User, Host, Password) ->
{atomic, ok} ->
{ok, io_lib:format("User ~s@~s successfully registered", [User, Host])};
{atomic, exists} ->
- String = io_lib:format("User ~s@~s already registered at node ~p",
- [User, Host, node()]),
- {exists, String};
+ Msg = io_lib:format("User ~s@~s already registered", [User, Host]),
+ {error, conflict, 10090, Msg};
{error, Reason} ->
String = io_lib:format("Can't register user ~s@~s at node ~p: ~p",
[User, Host, node(), Reason]),
- {cannot_register, String}
+ {error, cannot_register, 10001, String}
end.
unregister(User, Host) ->
@@ -402,7 +403,8 @@ registered_vhosts() ->
reload_config() ->
ejabberd_config:reload_file(),
acl:start(),
- shaper:start().
+ shaper:start(),
+ ejabberd_access_permissions:invalidate().
%%%
%%% Cluster management
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index 703614f63..e88f24e1e 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -45,16 +45,19 @@ start(normal, _Args) ->
write_pid_file(),
jid:start(),
start_apps(),
+ start_elixir_application(),
ejabberd:check_app(ejabberd),
randoms:start(),
db_init(),
start(),
translate:start(),
+ ejabberd_access_permissions:start_link(),
ejabberd_ctl:init(),
ejabberd_commands:init(),
ejabberd_admin:start(),
gen_mod:start(),
ext_mod:start(),
+ setup_if_elixir_conf_used(),
ejabberd_config:start(),
set_settings_from_config(),
acl:start(),
@@ -74,6 +77,7 @@ start(normal, _Args) ->
ejabberd_oauth:start(),
gen_mod:start_modules(),
ejabberd_listener:start_listeners(),
+ register_elixir_config_hooks(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
start(_, _) ->
@@ -221,6 +225,7 @@ start_apps() ->
ejabberd:start_app(fast_tls),
ejabberd:start_app(fast_xml),
ejabberd:start_app(stringprep),
+ http_p1:start(),
ejabberd:start_app(cache_tab).
opt_type(net_ticktime) ->
@@ -237,3 +242,26 @@ opt_type(modules) ->
Mods)
end;
opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime].
+
+setup_if_elixir_conf_used() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config.Store':start_link();
+ false -> ok
+ end.
+
+register_elixir_config_hooks() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config':start_hooks();
+ false -> ok
+ end.
+
+start_elixir_application() ->
+ case ejabberd_config:is_elixir_enabled() of
+ true ->
+ case application:ensure_started(elixir) of
+ ok -> ok;
+ {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
+ end;
+ _ ->
+ ok
+ end.
diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl
index 2a4554d15..f36c9fbc7 100644
--- a/src/ejabberd_auth_mnesia.erl
+++ b/src/ejabberd_auth_mnesia.erl
@@ -450,7 +450,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl
index c74f1b28e..05add262e 100644
--- a/src/ejabberd_auth_riak.erl
+++ b/src/ejabberd_auth_riak.erl
@@ -270,7 +270,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl
index d6d945e02..93dac4f4f 100644
--- a/src/ejabberd_auth_sql.erl
+++ b/src/ejabberd_auth_sql.erl
@@ -406,7 +406,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl
index 986310546..7ef708d31 100644
--- a/src/ejabberd_c2s.erl
+++ b/src/ejabberd_c2s.erl
@@ -32,6 +32,7 @@
-protocol({xep, 78, '2.5'}).
-protocol({xep, 138, '2.0'}).
-protocol({xep, 198, '1.3'}).
+-protocol({xep, 356, '7.1'}).
-update_info({update, 0}).
@@ -48,10 +49,16 @@
send_element/2,
socket_type/0,
get_presence/1,
+ get_last_presence/1,
get_aux_field/2,
set_aux_field/3,
del_aux_field/2,
get_subscription/2,
+ get_queued_stanzas/1,
+ get_csi_state/1,
+ set_csi_state/2,
+ get_resume_timeout/1,
+ set_resume_timeout/2,
send_filtered/5,
broadcast/4,
get_subscribed/1,
@@ -112,9 +119,12 @@
mgmt_pending_since,
mgmt_timeout,
mgmt_max_timeout,
+ mgmt_ack_timeout,
+ mgmt_ack_timer,
mgmt_resend,
mgmt_stanzas_in = 0,
mgmt_stanzas_out = 0,
+ mgmt_stanzas_req = 0,
ask_offline = true,
lang = <<"">>}).
@@ -182,6 +192,9 @@ socket_type() -> xml_stream.
get_presence(FsmRef) ->
(?GEN_FSM):sync_send_all_state_event(FsmRef,
{get_presence}, 1000).
+get_last_presence(FsmRef) ->
+ (?GEN_FSM):sync_send_all_state_event(FsmRef,
+ {get_last_presence}, 1000).
-spec get_aux_field(any(), state()) -> {ok, any()} | error.
get_aux_field(Key, #state{aux_fields = Opts}) ->
@@ -218,6 +231,27 @@ get_subscription(LFrom, StateData) ->
true -> none
end.
+get_queued_stanzas(#state{mgmt_queue = Queue} = StateData) ->
+ lists:map(fun({_N, Time, El}) ->
+ add_resent_delay_info(StateData, El, Time)
+ end, queue:to_list(Queue)).
+
+get_csi_state(#state{csi_state = CsiState}) ->
+ CsiState.
+
+set_csi_state(#state{} = StateData, CsiState) ->
+ StateData#state{csi_state = CsiState};
+set_csi_state(FsmRef, CsiState) ->
+ FsmRef ! {set_csi_state, CsiState}.
+
+get_resume_timeout(#state{mgmt_timeout = Timeout}) ->
+ Timeout.
+
+set_resume_timeout(#state{} = StateData, Timeout) ->
+ StateData#state{mgmt_timeout = Timeout};
+set_resume_timeout(FsmRef, Timeout) ->
+ FsmRef ! {set_resume_timeout, Timeout}.
+
-spec send_filtered(pid(), binary(), jid(), jid(), stanza()) -> any().
send_filtered(FsmRef, Feature, From, To, Packet) ->
FsmRef ! {send_filtered, Feature, From, To, Packet}.
@@ -282,13 +316,18 @@ init([{SockMod, Socket}, Opts]) ->
_ -> 1000
end,
ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of
- Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout;
+ RTimeo when is_integer(RTimeo), RTimeo >= 0 -> RTimeo;
_ -> 300
end,
MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of
Max when is_integer(Max), Max >= ResumeTimeout -> Max;
_ -> ResumeTimeout
end,
+ AckTimeout = case proplists:get_value(ack_timeout, Opts) of
+ ATimeo when is_integer(ATimeo), ATimeo > 0 -> ATimeo * 1000;
+ infinity -> undefined;
+ _ -> 60000
+ end,
ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of
Resend when is_boolean(Resend) -> Resend;
if_offline -> if_offline;
@@ -312,6 +351,7 @@ init([{SockMod, Socket}, Opts]) ->
mgmt_max_queue = MaxAckQueue,
mgmt_timeout = ResumeTimeout,
mgmt_max_timeout = MaxResumeTimeout,
+ mgmt_ack_timeout = AckTimeout,
mgmt_resend = ResendOnTimeout},
{ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}.
@@ -1147,6 +1187,15 @@ handle_sync_event({get_presence}, _From, StateName,
Resource = StateData#state.resource,
Reply = {User, Resource, Show, Status},
fsm_reply(Reply, StateName, StateData);
+handle_sync_event({get_last_presence}, _From, StateName,
+ StateData) ->
+ User = StateData#state.user,
+ Server = StateData#state.server,
+ PresLast = StateData#state.pres_last,
+ Resource = StateData#state.resource,
+ Reply = {User, Server, Resource, PresLast},
+ fsm_reply(Reply, StateName, StateData);
+
handle_sync_event(get_subscribed, _From, StateName,
StateData) ->
Subscribed = (?SETS):to_list(StateData#state.pres_f),
@@ -1159,7 +1208,7 @@ handle_sync_event({resume_session, Time}, _From, _StateName,
StateData#state.user,
StateData#state.server,
StateData#state.resource),
- {stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}};
+ {stop, normal, {resume, StateData}, StateData#state{mgmt_state = resumed}};
handle_sync_event({resume_session, _Time}, _From, StateName,
StateData) ->
{reply, {error, <<"Previous session not found">>}, StateName, StateData};
@@ -1347,8 +1396,13 @@ handle_info({route, From, To, Packet}, StateName, StateData) when ?is_stanza(Pac
groupchat -> ok;
headline -> ok;
_ ->
- ejabberd_router:route_error(
- To, From, Packet, xmpp:err_service_unavailable())
+ case xmpp:has_subtag(Packet, #muc_user{}) of
+ true ->
+ ok;
+ false ->
+ ejabberd_router:route_error(
+ To, From, Packet, xmpp:err_service_unavailable())
+ end
end,
{false, StateData}
end
@@ -1444,8 +1498,24 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) ->
From, jid:make(USR), Packet)
end, lists:usort(Recipients)),
fsm_next_state(StateName, StateData);
+handle_info({set_csi_state, CsiState}, StateName, StateData) ->
+ fsm_next_state(StateName, StateData#state{csi_state = CsiState});
+handle_info({set_resume_timeout, Timeout}, StateName, StateData) ->
+ fsm_next_state(StateName, StateData#state{mgmt_timeout = Timeout});
handle_info(dont_ask_offline, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{ask_offline = false});
+handle_info(close, StateName, StateData) ->
+ ?DEBUG("Timeout waiting for stream management acknowledgement of ~s",
+ [jid:to_string(StateData#state.jid)]),
+ close(self()),
+ fsm_next_state(StateName, StateData#state{mgmt_ack_timer = undefined});
+handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) ->
+ %% This happens if the resume_session/1 request timed out; the new session
+ %% now receives the late response.
+ ?DEBUG("Received old session state for ~s after failed resumption",
+ [jid:to_string(OldStateData#state.jid)]),
+ handle_unacked_stanzas(OldStateData#state{mgmt_resend = false}),
+ fsm_next_state(StateName, StateData);
handle_info(Info, StateName, StateData) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
fsm_next_state(StateName, StateData).
@@ -1562,6 +1632,7 @@ send_text(StateData, Text) ->
send_element(StateData, El) when StateData#state.mgmt_state == pending ->
?DEBUG("Cannot send element while waiting for resumption: ~p", [El]);
send_element(StateData, #xmlel{} = El) when StateData#state.xml_socket ->
+ ?DEBUG("Send XML on stream = ~p", [fxml:element_to_binary(El)]),
(StateData#state.sockmod):send_xml(StateData#state.socket,
{xmlstreamelement, El});
send_element(StateData, #xmlel{} = El) ->
@@ -1585,8 +1656,8 @@ send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive ->
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending ->
mgmt_queue_add(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
- NewStateData = send_stanza_and_ack_req(StateData, Stanza),
- mgmt_queue_add(NewStateData, Stanza);
+ NewStateData = mgmt_queue_add(StateData, Stanza),
+ mgmt_send_stanza(NewStateData, Stanza);
send_stanza(StateData, Stanza) ->
send_element(StateData, Stanza),
StateData.
@@ -2101,13 +2172,25 @@ fsm_next_state(session_established, StateData) ->
?C2S_HIBERNATE_TIMEOUT};
fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) ->
{stop, normal, StateData};
-fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} =
- StateData) ->
+fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined,
+ sid = SID, jid = JID, ip = IP,
+ conn = Conn, auth_module = AuthModule,
+ server = Host} = StateData) ->
+ case StateData of
+ #state{mgmt_ack_timer = undefined} ->
+ ok;
+ #state{mgmt_ack_timer = Timer} ->
+ erlang:cancel_timer(Timer)
+ end,
?INFO_MSG("Waiting for resumption of stream for ~s",
- [jid:to_string(StateData#state.jid)]),
+ [jid:to_string(JID)]),
+ Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}],
+ NewStateData = ejabberd_hooks:run_fold(c2s_session_pending, Host, StateData,
+ [SID, JID, Info]),
{next_state, wait_for_resume,
- StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()},
- StateData#state.mgmt_timeout};
+ NewStateData#state{mgmt_state = pending,
+ mgmt_pending_since = os:timestamp()},
+ NewStateData#state.mgmt_timeout};
fsm_next_state(wait_for_resume, StateData) ->
Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since),
Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1),
@@ -2338,15 +2421,16 @@ handle_r(StateData) ->
-spec handle_a(state(), sm_a()) -> state().
handle_a(StateData, #sm_a{h = H}) ->
- check_h_attribute(StateData, H).
+ NewStateData = check_h_attribute(StateData, H),
+ maybe_renew_ack_request(NewStateData).
-spec handle_resume(state(), sm_resume()) -> {ok, state()} | error.
handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
R = case stream_mgmt_enabled(StateData) of
true ->
case inherit_session_state(StateData, PrevID) of
- {ok, InheritedState} ->
- {ok, InheritedState, H};
+ {ok, InheritedState, Info} ->
+ {ok, InheritedState, Info, H};
{error, Err, InH} ->
{error, #sm_failed{reason = 'item-not-found',
h = InH, xmlns = Xmlns}, Err};
@@ -2360,7 +2444,7 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
<<"XEP-0198 disabled">>}
end,
case R of
- {ok, ResumedState, NumHandled} ->
+ {ok, ResumedState, ResumedInfo, NumHandled} ->
NewState = check_h_attribute(ResumedState, NumHandled),
AttrXmlns = NewState#state.mgmt_xmlns,
AttrId = make_resume_id(NewState),
@@ -2374,11 +2458,16 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
end,
handle_unacked_stanzas(NewState, SendFun),
send_element(NewState, #sm_r{xmlns = AttrXmlns}),
- FlushedState = csi_flush_queue(NewState),
- NewStateData = FlushedState#state{csi_state = active},
+ NewState1 = csi_flush_queue(NewState),
+ NewState2 = ejabberd_hooks:run_fold(c2s_session_resumed,
+ StateData#state.server,
+ NewState1,
+ [NewState1#state.sid,
+ NewState1#state.jid,
+ ResumedInfo]),
?INFO_MSG("Resumed session for ~s",
- [jid:to_string(NewStateData#state.jid)]),
- {ok, NewStateData};
+ [jid:to_string(NewState2#state.jid)]),
+ {ok, NewState2};
{error, El, Msg} ->
send_element(StateData, El),
?INFO_MSG("Cannot resume session for ~s@~s: ~s",
@@ -2413,15 +2502,45 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El)
update_num_stanzas_in(StateData, _El) ->
StateData.
--spec send_stanza_and_ack_req(state(), stanza()) -> state().
-send_stanza_and_ack_req(StateData, Stanza) ->
- AckReq = #sm_r{xmlns = StateData#state.mgmt_xmlns},
- case send_element(StateData, Stanza) == ok andalso
- send_element(StateData, AckReq) == ok of
+mgmt_send_stanza(StateData, Stanza) ->
+ case send_element(StateData, Stanza) of
+ ok ->
+ maybe_request_ack(StateData);
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) ->
+ request_ack(StateData);
+maybe_request_ack(StateData) ->
+ StateData.
+
+request_ack(#state{mgmt_xmlns = Xmlns,
+ mgmt_ack_timeout = AckTimeout} = StateData) ->
+ AckReq = #sm_r{xmlns = Xmlns},
+ case {send_element(StateData, AckReq), AckTimeout} of
+ {ok, undefined} ->
+ ok;
+ {ok, Timeout} ->
+ Timer = erlang:send_after(Timeout, self(), close),
+ StateData#state{mgmt_ack_timer = Timer,
+ mgmt_stanzas_req = StateData#state.mgmt_stanzas_out};
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) ->
+ StateData;
+maybe_renew_ack_request(#state{mgmt_ack_timer = Timer,
+ mgmt_queue = Queue,
+ mgmt_stanzas_out = NumStanzasOut,
+ mgmt_stanzas_req = NumStanzasReq} = StateData) ->
+ erlang:cancel_timer(Timer),
+ case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of
true ->
- StateData;
+ request_ack(StateData#state{mgmt_ack_timer = undefined});
false ->
- StateData#state{mgmt_state = pending}
+ StateData#state{mgmt_ack_timer = undefined}
end.
-spec mgmt_queue_add(state(), xmpp_element()) -> state().
@@ -2473,7 +2592,12 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F)
fun({_, Time, Pkt}) ->
From = xmpp:get_from(Pkt),
To = xmpp:get_to(Pkt),
- F(From, To, Pkt, Time)
+ case {From, To} of
+ {#jid{}, #jid{}} ->
+ F(From, To, Pkt, Time);
+ {_, _} ->
+ ?DEBUG("Dropping stanza due to invalid JID(s)", [])
+ end
end, queue:to_list(Queue))
end;
handle_unacked_stanzas(_StateData, _F) ->
@@ -2540,7 +2664,8 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData)
[StateData, From,
StateData#state.jid, El]) of
true ->
- ok;
+ ?DEBUG("Dropping archived message stanza from ~p",
+ [jid:to_string(xmpp:get_from(El))]);
false ->
ReRoute(From, To, El, Time)
end
@@ -2580,7 +2705,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
OldPID ->
OldSID = {Time, OldPID},
case catch resume_session(OldSID) of
- {ok, OldStateData} ->
+ {resume, OldStateData} ->
NewSID = {Time, self()}, % Old time, new PID
Priority = case OldStateData#state.pres_last of
undefined ->
@@ -2604,13 +2729,13 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
pres_timestamp = OldStateData#state.pres_timestamp,
privacy_list = OldStateData#state.privacy_list,
aux_fields = OldStateData#state.aux_fields,
- csi_state = OldStateData#state.csi_state,
mgmt_xmlns = OldStateData#state.mgmt_xmlns,
mgmt_queue = OldStateData#state.mgmt_queue,
mgmt_timeout = OldStateData#state.mgmt_timeout,
mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in,
mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out,
- mgmt_state = active}};
+ mgmt_state = active,
+ csi_state = active}, Info};
{error, Msg} ->
{error, Msg};
_ ->
@@ -2623,7 +2748,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
-spec resume_session({integer(), pid()}) -> any().
resume_session({Time, PID}) ->
- (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 5000).
+ (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 15000).
-spec make_resume_id(state()) -> binary().
make_resume_id(StateData) ->
@@ -2640,11 +2765,11 @@ add_resent_delay_info(#state{server = From}, El, Time) ->
%%% XEP-0352
%%%----------------------------------------------------------------------
-spec csi_filter_stanza(state(), stanza()) -> state().
-csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
- Stanza) ->
+csi_filter_stanza(#state{csi_state = CsiState, jid = JID, server = Server} =
+ StateData, Stanza) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_filter_stanza, Server,
{StateData, [Stanza]},
- [Server, Stanza]),
+ [Server, JID, Stanza]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},
@@ -2652,9 +2777,11 @@ csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
StateData2#state{csi_state = CsiState}.
-spec csi_flush_queue(state()) -> state().
-csi_flush_queue(#state{csi_state = CsiState, server = Server} = StateData) ->
+csi_flush_queue(#state{csi_state = CsiState, jid = JID, server = Server} =
+ StateData) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_flush_queue, Server,
- {StateData, []}, [Server]),
+ {StateData, []},
+ [Server, JID]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 9d41f50c2..8d74ad5a2 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -218,22 +218,26 @@
get_command_format/1,
get_command_format/2,
get_command_format/3,
- get_command_policy/1,
+ get_command_policy_and_scope/1,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
- get_commands/0,
+ get_exposed_commands/0,
register_commands/1,
unregister_commands/1,
+ expose_commands/1,
execute_command/2,
execute_command/3,
execute_command/4,
execute_command/5,
execute_command/6,
- opt_type/1,
- get_commands_spec/0
- ]).
+ opt_type/1,
+ get_commands_spec/0,
+ get_commands_definition/0,
+ get_commands_definition/1,
+ execute_command2/3,
+ execute_command2/4]).
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
@@ -273,28 +277,32 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
init() ->
- mnesia:delete_table(ejabberd_commands),
mnesia:create_table(ejabberd_commands,
- [{ram_copies, [node()]},
+ [{ram_copies, [node()]},
{local_content, true},
- {attributes, record_info(fields, ejabberd_commands)},
- {type, bag}]),
+ {attributes, record_info(fields, ejabberd_commands)},
+ {type, bag}]),
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
- register_commands(get_commands_spec()).
+ register_commands(get_commands_spec()),
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0).
-spec register_commands([ejabberd_commands()]) -> ok.
%% @doc Register ejabberd commands.
%% If a command is already registered, a warning is printed and the
%% old command is preserved.
+%% A registered command is not directly available to be called through
+%% ejabberd ReST API. It need to be exposed to be available through API.
register_commands(Commands) ->
lists:foreach(
fun(Command) ->
- % XXX check if command exists
- mnesia:dirty_write(Command)
- % ?DEBUG("This command is already defined:~n~p", [Command])
+ %% XXX check if command exists
+ mnesia:dirty_write(Command)
+ %% ?DEBUG("This command is already defined:~n~p", [Command])
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
-spec unregister_commands([ejabberd_commands()]) -> ok.
@@ -304,7 +312,28 @@ unregister_commands(Commands) ->
fun(Command) ->
mnesia:dirty_delete_object(Command)
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
+
+%% @doc Expose command through ejabberd ReST API.
+%% Pass a list of command names or policy to expose.
+-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}.
+
+expose_commands(Commands) ->
+ Names = lists:map(fun(#ejabberd_commands{name = Name}) ->
+ Name;
+ (Name) when is_atom(Name) ->
+ Name
+ end,
+ Commands),
+
+ case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of
+ {aborted, Reason} ->
+ {error, Reason};
+ {atomic, Result} ->
+ Result
+ end.
-spec list_commands() -> [{atom(), [aterm()], string()}].
@@ -319,8 +348,8 @@ list_commands() ->
list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc} <- Commands].
+ args = Args,
+ desc = Desc} <- Commands].
-spec list_commands_policy(integer()) ->
@@ -331,10 +360,10 @@ list_commands(Version) ->
list_commands_policy(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} ||
- #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc,
- policy = Policy} <- Commands].
+ #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc,
+ policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
@@ -356,27 +385,33 @@ get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args,
result = Result,
- policy = Policy} =
- get_command_definition(Name, Version),
+ policy = Policy} =
+ get_command_definition(Name, Version),
case Policy of
- user when Admin;
- Auth == noauth ->
- {[{user, binary}, {server, binary} | Args], Result};
- _ ->
- {Args, Result}
+ user when Admin;
+ Auth == noauth ->
+ {[{user, binary}, {server, binary} | Args], Result};
+ _ ->
+ {Args, Result}
end.
--spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}.
+-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
%% @doc return command policy.
-get_command_policy(Name) ->
+get_command_policy_and_scope(Name) ->
case get_command_definition(Name) of
- #ejabberd_commands{policy = Policy} ->
- {ok, Policy};
+ #ejabberd_commands{policy = Policy} = Cmd ->
+ {ok, Policy, cmd_scope(Cmd)};
command_not_found ->
{error, command_not_found}
end.
+%% The oauth scopes for a command are the command name itself,
+%% also might include either 'ejabberd:user' or 'ejabberd:admin'
+cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
+ [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
+
+
-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
@@ -388,46 +423,61 @@ get_command_definition(Name) ->
%% @doc Get the definition record of a command in a given API version.
get_command_definition(Name, Version) ->
case lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = N, version = V} = C)
- when N == Name, V =< Version ->
- {V, C}
- end)))) of
- [{_, Command} | _ ] -> Command;
- _E -> throw(unknown_command)
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = N, version = V} = C)
+ when N == Name, V =< Version ->
+ {V, C}
+ end)))) of
+ [{_, Command} | _ ] -> Command;
+ _E -> throw({error, unknown_command})
end.
+get_commands_definition() ->
+ get_commands_definition(?DEFAULT_VERSION).
+
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
% @doc Returns all commands for a given API version
get_commands_definition(Version) ->
L = lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = Name, version = V} = C)
- when V =< Version ->
- {Name, V, C}
- end)))),
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = Name, version = V} = C)
+ when V =< Version ->
+ {Name, V, C}
+ end)))),
F = fun({_Name, _V, Command}, []) ->
- [Command];
- ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
- Acc;
- ({_Name, _V, Command}, Acc) -> [Command | Acc]
- end,
+ [Command];
+ ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
+ Acc;
+ ({_Name, _V, Command}, Acc) -> [Command | Acc]
+ end,
lists:foldl(F, [], L).
+execute_command2(Name, Arguments, CallerInfo) ->
+ execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
+
+execute_command2(Name, Arguments, CallerInfo, Version) ->
+ Command = get_command_definition(Name, Version),
+ case ejabberd_access_permissions:can_access(Name, CallerInfo) of
+ allow ->
+ do_execute_command(Command, Arguments);
+ _ ->
+ throw({error, access_rules_unauthorized})
+ end.
+
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
-%% no_auth_provided
+%% no_auth_provided | access_rules_unauthorized
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
@@ -488,41 +538,64 @@ execute_command(AccessCommands, Auth, Name, Arguments) ->
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
+%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
-execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
+ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
Auth = case is_admin(Name, Auth1, CallerInfo) of
true -> admin;
false -> Auth1
end,
+ TokenJID = oauth_token_user(Auth1),
Command = get_command_definition(Name, Version),
- AccessCommands = get_access_commands(AccessCommands1, Version),
+ AccessCommands = get_all_access_commands(AccessCommands1),
+
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
- ok -> execute_command2(Auth, Command, Arguments)
+ ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
+ end.
+
+
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ noauth, _JID, Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, [User, Server | Arguments]).
+
+execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_access(undefined, _Command, _Arguments) ->
+ throw({error, access_rules_unauthorized});
+execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) ->
+ %% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
+ Host = global,
+ Rules = lists:map(fun({Mod, AccessName, Default}) ->
+ gen_mod:get_module_opt(Host, Mod,
+ AccessName, fun(A) -> A end, Default);
+ (Default) ->
+ Default
+ end, AccessRefs),
+ case acl:any_rules_allowed(Host, Rules, FromJID) of
+ true ->
+ do_execute_command(Command, Arguments);
+ false ->
+ throw({error, access_rules_unauthorized})
end.
-execute_command2(
- _Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- noauth, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, [User, Server | Arguments]).
-
-execute_command2(Command, Arguments) ->
+do_execute_command(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
@@ -592,31 +665,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
Command1
end,
AccessCommandsAllowed =
- lists:filter(
- fun({Access, Commands, ArgumentRestrictions}) ->
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end;
- ({Access, Commands}) ->
- ArgumentRestrictions = [],
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end
- end,
- AccessCommands),
+ lists:filter(
+ fun({Access, Commands, ArgumentRestrictions}) ->
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end;
+ ({Access, Commands}) ->
+ ArgumentRestrictions = [],
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end
+ end,
+ AccessCommands),
case AccessCommandsAllowed of
- [] -> throw({error, account_unprivileged});
- L when is_list(L) -> ok
+ [] -> throw({error, account_unprivileged});
+ L when is_list(L) -> ok
end.
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
@@ -627,11 +700,11 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
check_auth(_Command, noauth) ->
no_auth_provided;
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
- Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
- case ejabberd_oauth:check_token(User, Server, Scope, Token) of
+ ScopeList = cmd_scope(Command),
+ case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
true ->
{ok, User, Server};
- false ->
+ _ ->
throw({error, invalid_account_data})
end;
check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->
@@ -680,9 +753,9 @@ check_access2(Access, AccessInfo, Server) ->
check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of
- true -> check_access_arguments(Command, ArgumentRestrictions,
- Arguments);
- false -> false
+ true -> check_access_arguments(Command, ArgumentRestrictions,
+ Arguments);
+ false -> false
end.
check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
@@ -705,19 +778,23 @@ tag_arguments(ArgsDefs, Args) ->
Args).
+%% Get commands for all version
+get_all_access_commands(AccessCommands) ->
+ get_access_commands(AccessCommands, ?DEFAULT_VERSION).
+
get_access_commands(undefined, Version) ->
- Cmds = get_commands(Version),
+ Cmds = get_exposed_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
get_access_commands(AccessCommands, _Version) ->
AccessCommands.
-get_commands() ->
- get_commands(?DEFAULT_VERSION).
-get_commands(Version) ->
+get_exposed_commands() ->
+ get_exposed_commands(?DEFAULT_VERSION).
+get_exposed_commands(Version) ->
Opts0 = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
- []),
+ []),
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
@@ -727,31 +804,38 @@ get_commands(Version) ->
Cmds =
lists:foldl(
fun([{add_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
lists:usort(Cmds ++ Acc);
([{remove_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
Acc -- Cmds;
(_, Acc) -> Acc
- end, AdminCmds ++ UserCmds, Opts),
+ end, [], Opts),
Cmds.
+%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry
+expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
+ lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc;
+ (user, Acc) -> UserCmds ++ Acc;
+ (admin, Acc) -> AdminCmds ++ Acc;
+ (restricted, Acc) -> RestrictedCmds ++ Acc;
+ (Command, Acc) when is_atom(Command) ->
+ [Command|Acc]
+ end, [], L).
+
+oauth_token_user(noauth) ->
+ undefined;
+oauth_token_user(admin) ->
+ undefined;
+oauth_token_user({User, Server, _, _}) ->
+ jid:make(User, Server, <<>>).
+
is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
false;
+is_admin(_Name, Map, _extra) when is_map(Map) ->
+ true;
is_admin(Name, Auth, Extra) ->
{ACLInfo, Server} = case Auth of
{U, S, _, _} ->
@@ -773,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
deny -> false
end.
+permission_addon() ->
+ [{<<"'commands' option compatibility shim">>,
+ {[],
+ [{access, ejabberd_config:get_option(commands_admin_access,
+ fun(V) -> V end,
+ none)}],
+ {get_exposed_commands(), []}}}].
+
opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
opt_type(commands) ->
fun(V) when is_list(V) -> V end;
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index d82e32b9a..af26767f8 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -29,14 +29,16 @@
-export([start/0, load_file/1, reload_file/0, read_file/1,
add_global_option/2, add_local_option/2,
get_global_option/2, get_local_option/2,
- get_global_option/3, get_local_option/3,
- get_option/2, get_option/3, add_option/2, has_option/1,
- get_vh_by_auth_method/1, is_file_readable/1,
- get_version/0, get_myhosts/0, get_mylang/0,
- prepare_opt_val/4, convert_table_to_binary/5,
- transform_options/1, collect_options/1, default_db/2,
- convert_to_yaml/1, convert_to_yaml/2, v_db/2,
- env_binary_to_list/2, opt_type/1, may_hide_data/1]).
+ get_global_option/3, get_local_option/3,
+ get_option/2, get_option/3, add_option/2, has_option/1,
+ get_vh_by_auth_method/1, is_file_readable/1,
+ get_version/0, get_myhosts/0, get_mylang/0,
+ get_ejabberd_config_path/0, is_using_elixir_config/0,
+ prepare_opt_val/4, convert_table_to_binary/5,
+ transform_options/1, collect_options/1, default_db/2,
+ convert_to_yaml/1, convert_to_yaml/2, v_db/2,
+ env_binary_to_list/2, opt_type/1, may_hide_data/1,
+ is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1]).
-export([start/2]).
@@ -147,7 +149,18 @@ read_file(File) ->
{include_modules_configs, true}]).
read_file(File, Opts) ->
- Terms1 = get_plain_terms_file(File, Opts),
+ Terms1 = case is_elixir_enabled() of
+ true ->
+ case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
+ true ->
+ 'Elixir.Ejabberd.Config':init(File),
+ 'Elixir.Ejabberd.Config':get_ejabberd_opts();
+ false ->
+ get_plain_terms_file(File, Opts)
+ end;
+ false ->
+ get_plain_terms_file(File, Opts)
+ end,
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
true -> replace_macros(Terms1);
false -> Terms1
@@ -165,7 +178,8 @@ read_file(File, Opts) ->
-spec load_file(string()) -> ok.
load_file(File) ->
- State = read_file(File),
+ State0 = read_file(File),
+ State = validate_opts(State0),
set_opts(State).
-spec reload_file() -> ok.
@@ -318,7 +332,9 @@ get_absolute_path(File) ->
File;
relative ->
{ok, Dir} = file:get_cwd(),
- filename:absname_join(Dir, File)
+ filename:absname_join(Dir, File);
+ volumerelative ->
+ filename:absname(File)
end.
@@ -877,7 +893,20 @@ v_db(Mod, Type) ->
[] -> erlang:error(badarg)
end.
--spec default_db(global | binary(), module()) -> atom().
+-spec v_dbs(module()) -> [atom()].
+
+v_dbs(Mod) ->
+ lists:flatten(ets:match(module_db, {Mod, '$1'})).
+
+-spec v_dbs_mods(module()) -> [module()].
+
+v_dbs_mods(Mod) ->
+ lists:map(fun([M]) ->
+ binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_",
+ (atom_to_binary(M, utf8))/binary>>, utf8)
+ end, ets:match(module_db, {Mod, '$1'})).
+
+-spec default_db(binary(), module()) -> atom().
default_db(Host, Module) ->
case ejabberd_config:get_option(
@@ -1010,7 +1039,6 @@ replace_module(mod_private_odbc) -> {mod_private, sql};
replace_module(mod_roster_odbc) -> {mod_roster, sql};
replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql};
replace_module(mod_vcard_odbc) -> {mod_vcard, sql};
-replace_module(mod_vcard_ldap) -> {mod_vcard, ldap};
replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, sql};
replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql};
replace_module(Module) ->
@@ -1041,6 +1069,23 @@ replace_modules(Modules) ->
%% Elixir module naming
%% ====================
+-ifdef(ELIXIR_ENABLED).
+is_elixir_enabled() ->
+ true.
+-else.
+is_elixir_enabled() ->
+ false.
+-endif.
+
+is_using_elixir_config() ->
+ case is_elixir_enabled() of
+ true ->
+ Config = get_ejabberd_config_path(),
+ 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config);
+ false ->
+ false
+ end.
+
%% If module name start with uppercase letter, this is an Elixir module:
is_elixir_module(Module) ->
case atom_to_list(Module) of
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index d52d1c0a9..a96a28016 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -212,7 +212,7 @@ process(["help" | Mode], Version) ->
end;
process(["--version", Arg | Args], _) ->
- Version =
+ Version =
try
list_to_integer(Arg)
catch _:_ ->
@@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
- Result = ejabberd_commands:execute_command(AccessCommands,
- Auth, Command,
- ArgsFormatted,
- Version),
+ CI = case Auth of
+ {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
+ _ -> #{}
+ end,
+ CI2 = CI#{caller_module => ?MODULE},
+ Result = ejabberd_commands:execute_command2(Command,
+ ArgsFormatted,
+ CI2,
+ Version),
format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} =
@@ -374,6 +379,12 @@ format_arg2(Arg, Parse)->
format_result({error, ErrorAtom}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
+%% An error should always be allowed to return extended error to help with API.
+%% Extended error is of the form:
+%% {error, type :: atom(), code :: int(), Desc :: string()}
+format_result({error, ErrorAtom, Code, _Msg}, _) ->
+ {io_lib:format("Error: ~p", [ErrorAtom]), make_status(Code)};
+
format_result(Atom, {_Name, atom}) ->
io_lib:format("~p", [Atom]);
@@ -433,6 +444,8 @@ format_result(404, {_Name, _}) ->
make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
+make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
+make_status(Code) when is_integer(Code), Code > 0 -> Code;
make_status(_Error) -> ?STATUS_ERROR.
get_list_commands(Version) ->
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index 35679ccd3..c6c31a971 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) ->
DefinedHandlers = gen_mod:get_opt(
request_handlers, Opts,
fun(Hs) ->
+ Hs1 = lists:map(fun
+ ({Mod, Path}) when is_atom(Mod) -> {Path, Mod};
+ ({Path, Mod}) -> {Path, Mod}
+ end, Hs),
+
[{str:tokens(
iolist_to_binary(Path), <<"/">>),
- Mod} || {Path, Mod} <- Hs]
+ Mod} || {Path, Mod} <- Hs1]
end, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC,
@@ -391,7 +396,9 @@ extract_path_query(#state{request_method = Method,
socket = _Socket} = State)
when (Method =:= 'POST' orelse Method =:= 'PUT') andalso
is_integer(Len) ->
- {NewState, Data} = recv_data(State, Len),
+ case recv_data(State, Len) of
+ error -> {State, false};
+ {NewState, Data} ->
?DEBUG("client data: ~p~n", [Data]),
case catch url_decode_q_split(Path) of
{'EXIT', _} -> {NewState, false};
@@ -403,6 +410,7 @@ extract_path_query(#state{request_method = Method,
LQ -> LQ
end,
{NewState, {LPath, LQuery, Data}}
+ end
end;
extract_path_query(State) ->
{State, false}.
@@ -520,7 +528,7 @@ recv_data(State, Len, Acc) ->
recv_data(State, Len - byte_size(Data), <<Acc/binary, Data/binary>>);
Err ->
?DEBUG("Cannot receive HTTP data: ~p", [Err]),
- <<"">>
+ error
end;
_ ->
Trail = (State#state.trail),
@@ -763,7 +771,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
undefined;
Pos ->
{User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1),
- {User, Pass}
+ PassUtf8 = unicode:characters_to_binary(binary_to_list(Pass), utf8),
+ {User, PassUtf8}
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
Token = str:strip(SToken),
diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl
index 20edaa178..db529e69e 100644
--- a/src/ejabberd_http_bind.erl
+++ b/src/ejabberd_http_bind.erl
@@ -336,8 +336,9 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
init([Sid, Key, IP, HOpts]) ->
?DEBUG("started: ~p", [{Sid, Key, IP}]),
Opts1 = ejabberd_c2s_config:get_c2s_limits(),
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl
index 02df19e63..b92345dd4 100644
--- a/src/ejabberd_http_ws.erl
+++ b/src/ejabberd_http_ws.erl
@@ -112,8 +112,9 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl
index c2bf453a5..210575e5e 100644
--- a/src/ejabberd_local.erl
+++ b/src/ejabberd_local.erl
@@ -184,6 +184,8 @@ refresh_iq_handlers() ->
ejabberd_local ! refresh_iq_handlers.
-spec bounce_resource_packet(jid(), jid(), stanza()) -> ok.
+bounce_resource_packet(_From, _To, #presence{}) ->
+ ok;
bounce_resource_packet(From, To, Packet) ->
Lang = xmpp:get_lang(Packet),
Txt = <<"No available resource found">>,
@@ -282,25 +284,16 @@ do_route(From, To, Packet) ->
?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket "
"~P~n",
[From, To, Packet, 8]),
+ Type = xmpp:get_type(Packet),
if To#jid.luser /= <<"">> ->
ejabberd_sm:route(From, To, Packet);
- To#jid.lresource == <<"">> ->
- case Packet of
- #iq{} ->
- process_iq(From, To, Packet);
- #message{type = T} when T /= headline, T /= error ->
- Err = xmpp:make_error(Packet, xmpp:err_service_unavailable()),
- ejabberd_router:route(To, From, Err);
- _ -> ok
- end;
+ is_record(Packet, iq), To#jid.lresource == <<"">> ->
+ process_iq(From, To, Packet);
+ Type == result; Type == error; Type == headline ->
+ ok;
true ->
- case xmpp:get_type(Packet) of
- error -> ok;
- result -> ok;
- _ ->
- ejabberd_hooks:run(local_send_to_resource_hook,
- To#jid.lserver, [From, To, Packet])
- end
+ ejabberd_hooks:run(local_send_to_resource_hook,
+ To#jid.lserver, [From, To, Packet])
end.
-spec update_table() -> ok.
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 4e9dc8d08..318feb3f8 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -39,16 +39,17 @@
authenticate_user/2,
authenticate_client/2,
verify_resowner_scope/3,
- verify_client_scope/3,
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
+ check_token/1,
check_token/4,
check_token/2,
+ scope_in_scope_list/2,
process/2,
opt_type/1]).
--export([oauth_issue_token/1, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
+-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
-include("xmpp.hrl").
@@ -57,6 +58,7 @@
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
+-include("ejabberd_oauth.hrl").
-include("ejabberd_commands.hrl").
@@ -65,23 +67,30 @@
%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
--record(oauth_token, {
- token = {<<"">>, <<"">>} :: {binary(), binary()},
- us = {<<"">>, <<"">>} :: {binary(), binary()} | server_admin,
- scope = [] :: [binary()],
- expire :: integer()
- }).
--define(EXPIRE, 3600).
+-define(EXPIRE, 4294967).
start() ->
- init_db(mnesia, ?MYNAME),
+ DBMod = get_db_backend(),
+ DBMod:init(),
+ MaxSize =
+ ejabberd_config:get_option(
+ oauth_cache_size,
+ fun(I) when is_integer(I), I>0 -> I end,
+ 1000),
+ LifeTime =
+ ejabberd_config:get_option(
+ oauth_cache_life_time,
+ fun(I) when is_integer(I), I>0 -> I end,
+ timer:hours(1) div 1000),
+ cache_tab:new(oauth_token,
+ [{max_size, MaxSize}, {life_time, LifeTime}]),
Expire = expire(),
application:set_env(oauth2, backend, ejabberd_oauth),
application:set_env(oauth2, expiry_time, Expire),
application:start(oauth2),
ChildSpec = {?MODULE, {?MODULE, start_link, []},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec),
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@@ -90,57 +99,63 @@ start() ->
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
- desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins",
+ desc = "Issue an oauth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
- args = [{scopes, string}],
+ args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
- args_example = ["connected_users_number;muc_online_rooms"],
- args_desc = ["List of scopes to allow, separated by ';'"],
+ args_example = ["user@server.com", "connected_users_number;muc_online_rooms"],
+ args_desc = ["Jid for which issue token",
+ "Time to live of generated token in seconds",
+ "List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
- desc = "List oauth tokens, their scope, and how many seconds remain until expirity",
+ desc = "List oauth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}}
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
- desc = "List scopes that can be granted to tokens generated through the command line",
+ desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow",
module = ?MODULE, function = oauth_list_scopes,
args = [],
policy = restricted,
- result = {scopes, {list, {scope, string}}}
+ result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for a token",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, string}],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}},
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
result_desc = "List of remaining tokens"
}
].
-oauth_issue_token(ScopesString) ->
+oauth_issue_token(Jid, TTLSeconds, ScopesString) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
- case oauth2:authorize_client_credentials(ejabberd_ctl, Scopes, none) of
- {ok, {_AppCtx, Authorization}} ->
- {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, none),
+ case jid:from_string(list_to_binary(Jid)) of
+ #jid{luser =Username, lserver = Server} ->
+ case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of
+ {ok, {_Ctx,Authorization}} ->
+ {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]),
{ok, AccessToken} = oauth2_response:access_token(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
{ok, VerifiedScope} = oauth2_response:scope(Response),
- {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"};
+ {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"};
{error, Error} ->
{error, Error}
+ end;
+ error ->
+ {error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
- Tokens = mnesia:dirty_match_object(#oauth_token{us = server_admin, _ = '_'}),
+ Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- [{Token, Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
- #oauth_token{token=Token, scope=Scope, expire=Expires} <- Tokens].
+ [{Token, jid:to_string(jid:make(U,S,<<>>)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
+ #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
oauth_revoke_token(Token) ->
@@ -148,8 +163,7 @@ oauth_revoke_token(Token) ->
oauth_list_tokens().
oauth_list_scopes() ->
- get_cmd_scopes().
-
+ [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())].
@@ -170,15 +184,8 @@ handle_cast(_Msg, State) -> {noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- F = fun() ->
- Ts = mnesia:select(
- oauth_token,
- [{#oauth_token{expire = '$1', _ = '_'},
- [{'<', '$1', TS}],
- ['$_']}]),
- lists:foreach(fun mnesia:delete_object/1, Ts)
- end,
- mnesia:async_dirty(F),
+ DBMod = get_db_backend(),
+ DBMod:clean(TS),
erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)),
self(), clean),
{noreply, State};
@@ -189,21 +196,11 @@ terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
-init_db(mnesia, _Host) ->
- mnesia:create_table(oauth_token,
- [{disc_copies, [node()]},
- {attributes,
- record_info(fields, oauth_token)}]),
- mnesia:add_table_copy(oauth_token, node(), disc_copies);
-init_db(_, _) ->
- ok.
-
-
get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}.
-authenticate_user({User, Server}, {password, Password} = Ctx) ->
+authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server, <<"">>) of
#jid{} = JID ->
Access =
@@ -213,12 +210,17 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
none),
case acl:match_rule(JID#jid.lserver, Access, JID) of
allow ->
+ case Ctx of
+ {password, Password} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
{ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end;
+ admin_generated ->
+ {ok, {Ctx, {user, User, Server}}}
+ end;
deny ->
{error, badpass}
end;
@@ -229,8 +231,8 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
- Cmds = ejabberd_commands:get_commands(),
- Cmds1 = [sasl_auth | Cmds],
+ Cmds = ejabberd_commands:get_exposed_commands(),
+ Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds],
RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(RegisteredScope)) of
@@ -244,93 +246,156 @@ verify_resowner_scope(_, _, _) ->
get_cmd_scopes() ->
- Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
- {ok, Policy} when Policy =/= restricted -> true;
- _ -> false
- end end,
- ejabberd_commands:get_commands()),
- [atom_to_binary(C, utf8) || C <- Cmds].
+ ScopeMap = lists:foldl(fun(Cmd, Accum) ->
+ case ejabberd_commands:get_command_policy_and_scope(Cmd) of
+ {ok, Policy, Scopes} when Policy =/= restricted ->
+ lists:foldl(fun(Scope, Accum2) ->
+ dict:append(Scope, Cmd, Accum2)
+ end, Accum, Scopes);
+ _ -> Accum
+ end end, dict:new(), ejabberd_commands:get_exposed_commands()),
+ ScopeMap.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
-verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
- RegisteredScope = get_cmd_scopes(),
- case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
- oauth2_priv_set:new(RegisteredScope)) of
- true ->
- {ok, {Ctx, Scope}};
- false ->
- {error, badscope}
- end.
+%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
+% RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
+% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
+% oauth2_priv_set:new(RegisteredScope)) of
+% true ->
+% {ok, {Ctx, Scope}};
+% false ->
+% {error, badscope}
+% end.
+-spec seconds_since_epoch(integer()) -> non_neg_integer().
+seconds_since_epoch(Diff) ->
+ {Mega, Secs, _} = os:timestamp(),
+ Mega * 1000000 + Secs + Diff.
+
associate_access_code(_AccessCode, _Context, AppContext) ->
%put(?ACCESS_CODE_TABLE, AccessCode, Context),
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
- %% Tokens generated using the API/WEB belongs to users and always include the user, server pair.
- %% Tokens generated form command line aren't tied to an user, and instead belongs to the ejabberd sysadmin
- US = case proplists:get_value(<<"resource_owner">>, Context, <<"">>) of
- {user, User, Server} -> {jid:nodeprep(User), jid:nodeprep(Server)};
- undefined -> server_admin
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
+ Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
+ undefined ->
+ proplists:get_value(<<"expiry_time">>, Context, 0);
+ ExpiresIn ->
+ %% There is no clean way in oauth2 lib to actually override the TTL of the generated token.
+ %% It always pass the global configured value. Here we use the app context to pass the per-case
+ %% ttl if we want to override it.
+ seconds_since_epoch(ExpiresIn)
end,
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Scope = proplists:get_value(<<"scope">>, Context, []),
- Expire = proplists:get_value(<<"expiry_time">>, Context, 0),
R = #oauth_token{
token = AccessToken,
- us = US,
+ us = {jid:nodeprep(User), jid:nodeprep(Server)},
scope = Scope,
expire = Expire
},
- mnesia:dirty_write(R),
+ store(R),
{ok, AppContext}.
associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
+scope_in_scope_list(Scope, ScopeList) ->
+ TokenScopeSet = oauth2_priv_set:new(Scope),
+ lists:any(fun(Scope2) ->
+ oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
+ ScopeList).
+
+check_token(Token) ->
+ case lookup(Token) of
+ {ok, #oauth_token{us = US,
+ scope = TokenScope,
+ expire = Expire}} ->
+ {MegaSecs, Secs, _} = os:timestamp(),
+ TS = 1000000 * MegaSecs + Secs,
+ if
+ Expire > TS ->
+ {ok, US, TokenScope};
+ true ->
+ {false, expired}
+ end;
+ _ ->
+ {false, not_found}
+ end.
-check_token(User, Server, Scope, Token) ->
+check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = {LUser, LServer},
+ case lookup(Token) of
+ {ok, #oauth_token{us = {LUser, LServer},
scope = TokenScope,
- expire = Expire}] ->
+ expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS;
+ if
+ Expire > TS ->
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList);
+ true ->
+ {false, expired}
+ end;
_ ->
- false
+ {false, not_found}
end.
-check_token(Scope, Token) ->
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = US,
+check_token(ScopeList, Token) ->
+ case lookup(Token) of
+ {ok, #oauth_token{us = US,
scope = TokenScope,
- expire = Expire}] ->
+ expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- case oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS of
- true -> case US of
- {LUser, LServer} -> {ok, user, {LUser, LServer}};
- server_admin -> {ok, server_admin}
+ if
+ Expire > TS ->
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ case lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList) of
+ true -> {ok, user, US};
+ false -> {false, no_matching_scope}
end;
- false -> false
+ true ->
+ {false, expired}
end;
_ ->
- false
+ {false, not_found}
end.
+store(R) ->
+ cache_tab:insert(
+ oauth_token, R#oauth_token.token, R,
+ fun() ->
+ DBMod = get_db_backend(),
+ DBMod:store(R)
+ end).
+
+lookup(Token) ->
+ cache_tab:lookup(
+ oauth_token, Token,
+ fun() ->
+ DBMod = get_db_backend(),
+ case DBMod:lookup(Token) of
+ #oauth_token{} = R -> {ok, R};
+ _ -> error
+ end
+ end).
+
+
expire() ->
ejabberd_config:get_option(
oauth_expire,
@@ -358,12 +423,9 @@ process(_Handlers,
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
- [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]),
+ [?LABEL(<<"username">>, [?CT(<<"User (jid)">>), ?C(<<": ">>)]),
?INPUTID(<<"text">>, <<"username">>, <<"">>),
?BR,
- ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]),
- ?INPUTID(<<"text">>, <<"server">>, <<"">>),
- ?BR,
?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
@@ -372,6 +434,15 @@ process(_Handlers,
?INPUT(<<"hidden">>, <<"scope">>, Scope),
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
+ ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
+ ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
+ [
+ ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
+ ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
+ ?BR,
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
]),
Top =
@@ -415,11 +486,16 @@ process(_Handlers,
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
- Username = proplists:get_value(<<"username">>, Q, <<"">>),
- Server = proplists:get_value(<<"server">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
case oauth2:authorize_password({Username, Server},
ClientId,
RedirectURI,
@@ -427,10 +503,18 @@ process(_Handlers,
{password, Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
- oauth2:issue_token(Authorization, none),
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
%oauth2_wrq:redirected_access_token_response(ReqData,
% RedirectURI,
@@ -459,11 +543,82 @@ process(_Handlers,
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end;
+process(_Handlers,
+ #request{method = 'POST', q = Q, lang = _Lang,
+ path = [_, <<"token">>]}) ->
+ case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
+ <<"password">> ->
+ SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
+ Password = proplists:get_value(<<"password">>, Q, <<"">>),
+ Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
+ case oauth2:authorize_password({Username, Server},
+ Scope,
+ {password, Password}) of
+ {ok, {_AppContext, Authorization}} ->
+ {ok, {_AppContext2, Response}} =
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
+ {ok, AccessToken} = oauth2_response:access_token(Response),
+ {ok, Type} = oauth2_response:token_type(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
+ {ok, VerifiedScope} = oauth2_response:scope(Response),
+ json_response(200, {[
+ {<<"access_token">>, AccessToken},
+ {<<"token_type">>, Type},
+ {<<"scope">>, str:join(VerifiedScope, <<" ">>)},
+ {<<"expires_in">>, Expires}]});
+ {error, Error} when is_atom(Error) ->
+ json_error(400, <<"invalid_grant">>, Error)
+ end;
+ _OtherGrantType ->
+ json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
+ end;
+
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
+-spec get_db_backend() -> module().
+
+get_db_backend() ->
+ DBType = ejabberd_config:get_option(
+ oauth_db_type,
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end,
+ mnesia),
+ list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
+
+
+%% Headers as per RFC 6749
+json_response(Code, Body) ->
+ {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
+ {<<"Cache-Control">>, <<"no-store">>},
+ {<<"Pragma">>, <<"no-cache">>}],
+ jiffy:encode(Body)}.
+%% OAauth error are defined in:
+%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
+json_error(Code, Error, Reason) ->
+ Desc = json_error_desc(Reason),
+ Body = {[{<<"error">>, Error},
+ {<<"error_description">>, Desc}]},
+ json_response(Code, Body).
+json_error_desc(access_denied) -> <<"Access denied">>;
+json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
+json_error_desc(invalid_scope) -> <<"Invalid scope">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
@@ -595,4 +750,10 @@ opt_type(oauth_expire) ->
fun(I) when is_integer(I), I >= 0 -> I end;
opt_type(oauth_access) ->
fun acl:access_rules_validator/1;
-opt_type(_) -> [oauth_expire, oauth_access].
+opt_type(oauth_db_type) ->
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
+opt_type(oauth_cache_life_time) ->
+ fun (I) when is_integer(I), I > 0 -> I end;
+opt_type(oauth_cache_size) ->
+ fun (I) when is_integer(I), I > 0 -> I end;
+opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type].
diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl
new file mode 100644
index 000000000..a23f443ed
--- /dev/null
+++ b/src/ejabberd_oauth_mnesia.erl
@@ -0,0 +1,65 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_mnesia.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 mnesia backend
+%%% Created : 20 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_mnesia).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1]).
+
+-include("ejabberd_oauth.hrl").
+
+init() ->
+ mnesia:create_table(oauth_token,
+ [{disc_copies, [node()]},
+ {attributes,
+ record_info(fields, oauth_token)}]),
+ mnesia:add_table_copy(oauth_token, node(), disc_copies),
+ ok.
+
+store(R) ->
+ mnesia:dirty_write(R).
+
+lookup(Token) ->
+ case catch mnesia:dirty_read(oauth_token, Token) of
+ [R] ->
+ R;
+ _ ->
+ false
+ end.
+
+clean(TS) ->
+ F = fun() ->
+ Ts = mnesia:select(
+ oauth_token,
+ [{#oauth_token{expire = '$1', _ = '_'},
+ [{'<', '$1', TS}],
+ ['$_']}]),
+ lists:foreach(fun mnesia:delete_object/1, Ts)
+ end,
+ mnesia:async_dirty(F).
+
diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl
new file mode 100644
index 000000000..aadb97084
--- /dev/null
+++ b/src/ejabberd_oauth_rest.erl
@@ -0,0 +1,98 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_rest.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 REST backend
+%%% Created : 26 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_rest).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1,
+ opt_type/1]).
+
+-include("ejabberd.hrl").
+-include("ejabberd_oauth.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+
+init() ->
+ rest:start(?MYNAME),
+ ok.
+
+store(R) ->
+ Path = path(<<"store">>),
+ %% Retry 2 times, with a backoff of 500millisec
+ {User, Server} = R#oauth_token.us,
+ SJID = jid:to_string({User, Server, <<"">>}),
+ case rest:with_retry(
+ post,
+ [?MYNAME, Path, [],
+ {[{<<"token">>, R#oauth_token.token},
+ {<<"user">>, SJID},
+ {<<"scope">>, R#oauth_token.scope},
+ {<<"expire">>, R#oauth_token.expire}
+ ]}], 2, 500) of
+ {ok, Code, _} when Code == 200 orelse Code == 201 ->
+ ok;
+ Err ->
+ ?ERROR_MSG("failed to store oauth record ~p: ~p", [R, Err]),
+ {error, Err}
+ end.
+
+lookup(Token) ->
+ Path = path(<<"lookup">>),
+ case rest:with_retry(post, [?MYNAME, Path, [],
+ {[{<<"token">>, Token}]}],
+ 2, 500) of
+ {ok, 200, {Data}} ->
+ SJID = proplists:get_value(<<"user">>, Data, <<>>),
+ JID = jid:from_string(SJID),
+ US = {JID#jid.luser, JID#jid.lserver},
+ Scope = proplists:get_value(<<"scope">>, Data, []),
+ Expire = proplists:get_value(<<"expire">>, Data, 0),
+ #oauth_token{token = Token,
+ us = US,
+ scope = Scope,
+ expire = Expire};
+ {ok, 404, _Resp} ->
+ false;
+ Other ->
+ ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]),
+ {error, rest_failed}
+ end.
+
+clean(_TS) ->
+ ok.
+
+path(Path) ->
+ Base = ejabberd_config:get_option(ext_api_path_oauth,
+ fun(X) -> iolist_to_binary(X) end,
+ <<"/oauth">>),
+ <<Base/binary, "/", Path/binary>>.
+
+
+opt_type(ext_api_path_oauth) ->
+ fun (X) -> iolist_to_binary(X) end;
+opt_type(_) -> [ext_api_path_oauth].
diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl
new file mode 100644
index 000000000..9253335ff
--- /dev/null
+++ b/src/ejabberd_oauth_sql.erl
@@ -0,0 +1,78 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_sql.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 SQL backend
+%%% Created : 27 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_sql).
+
+-compile([{parse_transform, ejabberd_sql_pt}]).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1]).
+
+-include("ejabberd_oauth.hrl").
+-include("ejabberd.hrl").
+-include("ejabberd_sql_pt.hrl").
+-include("jlib.hrl").
+
+init() ->
+ ok.
+
+store(R) ->
+ Token = R#oauth_token.token,
+ {User, Server} = R#oauth_token.us,
+ SJID = jid:to_string({User, Server, <<"">>}),
+ Scope = str:join(R#oauth_token.scope, <<" ">>),
+ Expire = R#oauth_token.expire,
+ ?SQL_UPSERT(
+ ?MYNAME,
+ "oauth_token",
+ ["!token=%(Token)s",
+ "jid=%(SJID)s",
+ "scope=%(Scope)s",
+ "expire=%(Expire)d"]).
+
+lookup(Token) ->
+ case ejabberd_sql:sql_query(
+ ?MYNAME,
+ ?SQL("select @(jid)s, @(scope)s, @(expire)d"
+ " from oauth_token where token=%(Token)s")) of
+ {selected, [{SJID, Scope, Expire}]} ->
+ JID = jid:from_string(SJID),
+ US = {JID#jid.luser, JID#jid.lserver},
+ #oauth_token{token = Token,
+ us = US,
+ scope = str:tokens(Scope, <<" ">>),
+ expire = Expire};
+ _ ->
+ false
+ end.
+
+clean(TS) ->
+ ejabberd_sql:sql_query(
+ ?MYNAME,
+ ?SQL("delete from oauth_token where expire < %(TS)d")).
+
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 3c3e698ad..97aef3cab 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -466,19 +466,17 @@ send_element(Pid, El) ->
%%% ejabberd commands
get_commands_spec() ->
- [#ejabberd_commands{name = incoming_s2s_number,
+ [#ejabberd_commands{
+ name = incoming_s2s_number,
tags = [stats, s2s],
- desc =
- "Number of incoming s2s connections on "
- "the node",
+ desc = "Number of incoming s2s connections on the node",
policy = admin,
module = ?MODULE, function = incoming_s2s_number,
args = [], result = {s2s_incoming, integer}},
- #ejabberd_commands{name = outgoing_s2s_number,
+ #ejabberd_commands{
+ name = outgoing_s2s_number,
tags = [stats, s2s],
- desc =
- "Number of outgoing s2s connections on "
- "the node",
+ desc = "Number of outgoing s2s connections on the node",
policy = admin,
module = ?MODULE, function = outgoing_s2s_number,
args = [], result = {s2s_outgoing, integer}},
@@ -489,11 +487,19 @@ get_commands_spec() ->
module = ?MODULE, function = stop_all_connections,
args = [], result = {res, rescode}}].
+%% TODO Move those stats commands to ejabberd stats command ?
incoming_s2s_number() ->
- length(supervisor:which_children(ejabberd_s2s_in_sup)).
+ supervisor_count(ejabberd_s2s_in_sup).
outgoing_s2s_number() ->
- length(supervisor:which_children(ejabberd_s2s_out_sup)).
+ supervisor_count(ejabberd_s2s_out_sup).
+
+supervisor_count(Supervisor) ->
+ case catch supervisor:which_children(Supervisor) of
+ {'EXIT', _} -> 0;
+ Result ->
+ length(Result)
+ end.
stop_all_connections() ->
lists:foreach(
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index 62c07b068..076ba2d3b 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -850,13 +850,12 @@ get_addr_port(Server) ->
?DEBUG("srv lookup of '~s': ~p~n",
[Server, HEnt#hostent.h_addr_list]),
AddrList = HEnt#hostent.h_addr_list,
- random:seed(p1_time_compat:timestamp()),
case catch lists:map(fun ({Priority, Weight, Port,
Host}) ->
N = case Weight of
0 -> 0;
_ ->
- (Weight + 1) * random:uniform()
+ (Weight + 1) * randoms:uniform()
end,
{Priority * 65536 - N, Host, Port}
end,
diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl
index b1a4b433e..94cd68ecf 100644
--- a/src/ejabberd_service.erl
+++ b/src/ejabberd_service.erl
@@ -98,13 +98,13 @@ init([{SockMod, Socket}, Opts]) ->
fun({H, Os}, D) ->
P = proplists:get_value(
password, Os,
- p1_sha:sha(crypto:rand_bytes(20))),
+ p1_sha:sha(randoms:bytes(20))),
dict:store(H, P, D)
end, dict:new(), HOpts);
false ->
Pass = proplists:get_value(
password, Opts,
- p1_sha:sha(crypto:rand_bytes(20))),
+ p1_sha:sha(randoms:bytes(20))),
dict:from_list([{global, Pass}])
end,
Shaper = case lists:keysearch(shaper_rule, 1, Opts) of
@@ -170,27 +170,37 @@ wait_for_stream(closed, StateData) ->
wait_for_handshake({xmlstreamelement, El}, StateData) ->
decode_element(El, wait_for_handshake, StateData);
wait_for_handshake(#handshake{data = Digest}, StateData) ->
- case dict:find(StateData#state.host, StateData#state.host_opts) of
- {ok, Password} ->
- case p1_sha:sha(<<(StateData#state.streamid)/binary,
- Password/binary>>) of
- Digest ->
- send_element(StateData, #handshake{}),
- lists:foreach(
- fun (H) ->
- ejabberd_router:register_route(H, ?MYNAME),
- ?INFO_MSG("Route registered for service ~p~n",
- [H])
- end, dict:fetch_keys(StateData#state.host_opts)),
- {next_state, stream_established, StateData};
- _ ->
- send_element(StateData, xmpp:serr_not_authorized()),
- {stop, normal, StateData}
- end;
- _ ->
- send_element(StateData, xmpp:serr_not_authorized()),
- {stop, normal, StateData}
- end;
+ send_element(StateData, #handshake{}),
+ lists:foreach(
+ fun (H) ->
+ ejabberd_router:register_route(H, ?MYNAME),
+ ?INFO_MSG("Route registered for service ~p~n",
+ [H]),
+ ejabberd_hooks:run(component_connected, [H])
+ end, dict:fetch_keys(StateData#state.host_opts)),
+ {next_state, stream_established, StateData};
+ %% case dict:find(StateData#state.host, StateData#state.host_opts) of
+ %% {ok, Password} ->
+ %% case p1_sha:sha(<<(StateData#state.streamid)/binary,
+ %% Password/binary>>) of
+ %% Digest ->
+ %% send_element(StateData, #handshake{}),
+ %% lists:foreach(
+ %% fun (H) ->
+ %% ejabberd_router:register_route(H, ?MYNAME),
+ %% ?INFO_MSG("Route registered for service ~p~n",
+ %% [H]),
+ %% ejabberd_hooks:run(component_connected, [H])
+ %% end, dict:fetch_keys(StateData#state.host_opts)),
+ %% {next_state, stream_established, StateData};
+ %% _ ->
+ %% send_element(StateData, xmpp:serr_not_authorized()),
+ %% {stop, normal, StateData}
+ %% end;
+ %% _ ->
+ %% send_element(StateData, xmpp:serr_not_authorized()),
+ %% {stop, normal, StateData}
+ %% end;
wait_for_handshake({xmlstreamend, _Name}, StateData) ->
{stop, normal, StateData};
wait_for_handshake({xmlstreamerror, _}, StateData) ->
@@ -211,24 +221,10 @@ stream_established(El, StateData) when ?is_stanza(El) ->
Txt = <<"Missing 'from' or 'to' attribute">>,
send_error(StateData, El, xmpp:err_jid_malformed(Txt, Lang));
true ->
- FromJID = case StateData#state.check_from of
- false ->
- %% If the admin does not want to check the from field
- %% when accept packets from any address.
- %% In this case, the component can send packet of
- %% behalf of the server users.
- From;
- _ ->
- %% The default is the standard behaviour in XEP-0114
- Server = From#jid.lserver,
- case dict:is_key(Server, StateData#state.host_opts) of
- true -> From;
- false -> error
- end
- end,
- if FromJID /= error ->
- ejabberd_router:route(FromJID, To, El);
- true ->
+ case check_from(From, StateData) of
+ true ->
+ ejabberd_router:route(From, To, El);
+ false ->
Txt = <<"Improper domain part of 'from' attribute">>,
send_error(StateData, El, xmpp:err_not_allowed(Txt, Lang))
end
@@ -281,7 +277,9 @@ terminate(Reason, StateName, StateData) ->
case StateName of
stream_established ->
lists:foreach(fun (H) ->
- ejabberd_router:unregister_route(H)
+ ejabberd_router:unregister_route(H),
+ ejabberd_hooks:run(component_disconnected,
+ [H, Reason])
end,
dict:fetch_keys(StateData#state.host_opts));
_ -> ok
@@ -350,6 +348,18 @@ decode_element(#xmlel{} = El, StateName, StateData) ->
{next_state, StateName, StateData}
end.
+-spec check_from(jid(), state()) -> boolean().
+check_from(_From, #state{check_from = false}) ->
+ %% If the admin does not want to check the from field
+ %% when accept packets from any address.
+ %% In this case, the component can send packet of
+ %% behalf of the server users.
+ true;
+check_from(From, StateData) ->
+ %% The default is the standard behaviour in XEP-0114
+ Server = From#jid.lserver,
+ dict:is_key(Server, StateData#state.host_opts).
+
-spec new_id() -> binary().
new_id() -> randoms:get_string().
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index 0655bbcf3..18703dc9c 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -279,25 +279,28 @@ get_session_pid(User, Server, Resource) ->
-spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok.
-set_offline_info({Time, _Pid}, User, Server, Resource, Info) ->
- SID = {Time, undefined},
+set_offline_info(SID, User, Server, Resource, Info) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
- set_session(SID, LUser, LServer, LResource, undefined, Info).
+ set_session(SID, LUser, LServer, LResource, undefined, [offline | Info]).
-spec get_offline_info(erlang:timestamp(), binary(), binary(),
binary()) -> none | info().
get_offline_info(Time, User, Server, Resource) ->
- SID = {Time, undefined},
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
Mod = get_sm_backend(LServer),
case Mod:get_sessions(LUser, LServer, LResource) of
- [#session{sid = SID, info = Info}] ->
+ [#session{sid = {Time, _}, info = Info}] ->
+ case proplists:get_bool(offline, Info) of
+ true ->
Info;
+ false ->
+ none
+ end;
_ ->
none
end.
@@ -434,11 +437,12 @@ set_session(SID, User, Server, Resource, Priority, Info) ->
-spec online([#session{}]) -> [#session{}].
online(Sessions) ->
- lists:filter(fun(#session{sid = {_, undefined}}) ->
- false;
- (_) ->
- true
- end, Sessions).
+ lists:filter(fun is_online/1, Sessions).
+
+-spec is_online(#session{}) -> boolean().
+
+is_online(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
@@ -629,15 +633,17 @@ check_for_sessions_to_replace(User, Server, Resource) ->
-spec check_existing_resources(binary(), binary(), binary()) -> ok.
check_existing_resources(LUser, LServer, LResource) ->
- SIDs = get_resource_sessions(LUser, LServer, LResource),
- if SIDs == [] -> ok;
+ Mod = get_sm_backend(LServer),
+ Ss = Mod:get_sessions(LUser, LServer, LResource),
+ {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
+ lists:foreach(fun(#session{sid = S}) ->
+ Mod:delete_session(LUser, LServer, LResource, S)
+ end, OfflineSs),
+ if OnlineSs == [] -> ok;
true ->
+ SIDs = [SID || #session{sid = SID} <- OnlineSs],
MaxSID = lists:max(SIDs),
- lists:foreach(fun ({_, undefined} = S) ->
- Mod = get_sm_backend(LServer),
- Mod:delete_session(LUser, LServer, LResource,
- S);
- ({_, Pid} = S) when S /= MaxSID ->
+ lists:foreach(fun ({_, Pid} = S) when S /= MaxSID ->
Pid ! replaced;
(_) -> ok
end,
@@ -660,10 +666,18 @@ get_resource_sessions(User, Server, Resource) ->
-spec check_max_sessions(binary(), binary()) -> ok | replaced.
check_max_sessions(LUser, LServer) ->
Mod = get_sm_backend(LServer),
- SIDs = [S#session.sid || S <- online(Mod:get_sessions(LUser, LServer))],
+ Ss = Mod:get_sessions(LUser, LServer),
+ {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
MaxSessions = get_max_user_sessions(LUser, LServer),
- if length(SIDs) =< MaxSessions -> ok;
- true -> {_, Pid} = lists:min(SIDs), Pid ! replaced
+ if length(OnlineSs) =< MaxSessions -> ok;
+ true ->
+ #session{sid = {_, Pid}} = lists:min(OnlineSs),
+ Pid ! replaced
+ end,
+ if length(OfflineSs) =< MaxSessions -> ok;
+ true ->
+ #session{sid = SID, usr = {_, _, R}} = lists:min(OfflineSs),
+ Mod:delete_session(LUser, LServer, R, SID)
end.
%% Get the user_max_session setting
diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl
index 9c78acaf7..049f1de58 100644
--- a/src/ejabberd_sm_redis.erl
+++ b/src/ejabberd_sm_redis.erl
@@ -144,7 +144,10 @@ clean_table() ->
{_, SID} = binary_to_term(USSIDKey),
node(element(2, SID)) == node()
end, Vals),
- Q1 = ["HDEL", ServKey | Vals1],
+ Q1 = case Vals1 of
+ [] -> [];
+ _ -> ["HDEL", ServKey | Vals1]
+ end,
Q2 = lists:map(
fun(USSIDKey) ->
{US, SID} = binary_to_term(USSIDKey),
@@ -152,7 +155,7 @@ clean_table() ->
SIDKey = sid_to_key(SID),
["HDEL", USKey, SIDKey]
end, Vals1),
- Res = ejabberd_redis:qp([Q1|Q2]),
+ Res = ejabberd_redis:qp(lists:delete([], [Q1|Q2])),
case lists:filter(
fun({ok, _}) -> false;
(_) -> true
diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl
index 8116d617f..8db8b6c5f 100644
--- a/src/ejabberd_sql.erl
+++ b/src/ejabberd_sql.erl
@@ -629,7 +629,7 @@ generic_sql_query_format(SQLQuery) ->
generic_escape() ->
#sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end,
- integer = fun(X) -> integer_to_binary(X) end,
+ integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@@ -646,7 +646,7 @@ sqlite_sql_query_format(SQLQuery) ->
sqlite_escape() ->
#sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end,
- integer = fun(X) -> integer_to_binary(X) end,
+ integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@@ -670,7 +670,7 @@ pgsql_prepare(SQLQuery, State) ->
pgsql_execute_escape() ->
#sql_escape{string = fun(X) -> X end,
- integer = fun(X) -> [integer_to_binary(X)] end,
+ integer = fun(X) -> [jlib:i2l(X)] end,
boolean = fun(true) -> "1";
(false) -> "0"
end
@@ -790,7 +790,7 @@ pgsql_connect(Server, Port, DB, Username, Password) ->
{port, Port},
{as_binary, true}]) of
{ok, Ref} ->
- pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>,
+ pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>,
<<"standard_conforming_strings='off';">>]),
pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]),
{ok, Ref};
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 498fcf9b0..bf17c8ab1 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -74,20 +74,27 @@ get_acl_rule([<<"vhosts">>], _) ->
%% The pages of a vhost are only accesible if the user is admin of that vhost:
get_acl_rule([<<"server">>, VHost | _RPath], Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {VHost, [configure, webadmin_view]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {VHost, [AC, ACR]};
get_acl_rule([<<"server">>, VHost | _RPath], 'POST') ->
- {VHost, [configure]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {VHost, [AC]};
%% Default rule: only global admins can access any other random page
get_acl_rule(_RPath, Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {global, [configure, webadmin_view]};
-get_acl_rule(_RPath, 'POST') -> {global, [configure]}.
-
-is_acl_match(Host, Rules, Jid) ->
- lists:any(fun (Rule) ->
- allow == acl:match_rule(Host, Rule, Jid)
- end,
- Rules).
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {global, [AC, ACR]};
+get_acl_rule(_RPath, 'POST') ->
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {global, [AC]}.
%%%==================================
%%%% Menu Items Access
@@ -138,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) ->
is_allowed_path(Path, JID);
is_allowed_path(Path, JID) ->
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
- is_acl_match(HostOfRule, AccessRule, JID).
+ acl:any_rules_allowed(HostOfRule, AccessRule, JID).
%% @spec(Path) -> URL
%% where Path = [string()]
@@ -266,7 +273,7 @@ get_auth_account(HostOfRule, AccessRule, User, Server,
Pass) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true ->
- case is_acl_match(HostOfRule, AccessRule,
+ case acl:any_rules_allowed(HostOfRule, AccessRule,
jid:make(User, Server, <<"">>))
of
false -> {unauthorized, <<"unprivileged-account">>};
@@ -740,7 +747,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
- ets:select(acl,
+ mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
{NumLines, ACLsP} = term_to_paragraph(ACLs, 80),
@@ -777,7 +784,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
- ets:select(acl,
+ mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
make_xhtml((?H1GL((?T(<<"Access Control Lists">>)),
@@ -842,7 +849,7 @@ process_admin(Host,
end;
_ -> nothing
end,
- Access = ets:select(access,
+ Access = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
{NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80),
@@ -876,7 +883,7 @@ process_admin(Host,
end;
_ -> nothing
end,
- AccessRules = ets:select(access,
+ AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
make_xhtml((?H1GL((?T(<<"Access Rules">>)),
@@ -1150,7 +1157,7 @@ term_to_paragraph(T, Cols) ->
term_to_id(T) -> jlib:encode_base64((term_to_binary(T))).
acl_parse_query(Host, Query) ->
- ACLs = ets:select(acl,
+ ACLs = mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}]),
case lists:keysearch(<<"submit">>, 1, Query) of
@@ -1264,7 +1271,7 @@ access_rules_to_xhtml(AccessRules, Lang) ->
<<"Add New">>)])])]))]).
access_parse_query(Host, Query) ->
- AccessRules = ets:select(access,
+ AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
case lists:keysearch(<<"addnew">>, 1, Query) of
@@ -1337,7 +1344,7 @@ parse_access_rule(Text) ->
list_vhosts(Lang, JID) ->
Hosts = (?MYHOSTS),
HostsAllowed = lists:filter(fun (Host) ->
- is_acl_match(Host,
+ acl:any_rules_allowed(Host,
[configure, webadmin_view],
JID)
end,
@@ -1549,7 +1556,7 @@ su_to_list({Server, User}) ->
%%%% get_stats
get_stats(global, Lang) ->
- OnlineUsers = mnesia:table_info(session, size),
+ OnlineUsers = ejabberd_sm:connected_users_number(),
RegisteredUsers = lists:foldl(fun (Host, Total) ->
ejabberd_auth:get_vh_registered_users_number(Host)
+ Total
@@ -2175,7 +2182,7 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) ->
CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]),
CPUTimeS = list_to_binary(io_lib:format("~.3f",
[element(1, CPUTime) / 1000])),
- OnlineUsers = mnesia:table_info(session, size),
+ OnlineUsers = ejabberd_sm:connected_users_number(),
TransactionsCommitted = ejabberd_cluster:call(Node, mnesia,
system_info, [transaction_commits]),
TransactionsAborted = ejabberd_cluster:call(Node, mnesia,
@@ -2970,7 +2977,8 @@ make_menu_item(item, 3, URI, Name, Lang) ->
%%%==================================
-opt_type(access) -> fun (V) -> V end;
-opt_type(_) -> [access].
+opt_type(access) -> fun acl:access_rules_validator/1;
+opt_type(access_readonly) -> fun acl:access_rules_validator/1;
+opt_type(_) -> [access, access_readonly].
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl
index 6259b4efd..1b795d3fd 100644
--- a/src/ejabberd_xmlrpc.erl
+++ b/src/ejabberd_xmlrpc.erl
@@ -47,7 +47,8 @@
-record(state,
{access_commands = [] :: list(),
auth = noauth :: noauth | {binary(), binary(), binary()},
- get_auth = true :: boolean()}).
+ get_auth = true :: boolean(),
+ ip :: inet:ip_address()}).
%% Test:
@@ -195,7 +196,7 @@ socket_type() -> raw.
%% -----------------------------
%% HTTP interface
%% -----------------------------
-process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
+process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) ->
AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
fun(L) when is_list(L) -> L end,
undefined),
@@ -206,7 +207,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
lists:flatmap(
fun({Ac, AcOpts}) ->
Commands = gen_mod:get_opt(
- commands, AcOpts,
+ commands, lists:flatten(AcOpts),
fun(A) when is_atom(A) ->
A;
(L) when is_list(L) ->
@@ -219,15 +220,15 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
options, AcOpts,
fun(L) when is_list(L) -> L end,
[]),
- [{Ac, Commands, CommOpts}];
+ [{<<"ejabberd_xmlrpc compatibility shim">>, {[?MODULE], [{access, Ac}], Commands}}];
(Wrong) ->
?WARNING_MSG("wrong options format for ~p: ~p",
[?MODULE, Wrong]),
[]
- end, AccessCommandsOpts)
+ end, lists:flatten(AccessCommandsOpts))
end,
GetAuth = true,
- State = #state{access_commands = AccessCommands, get_auth = GetAuth},
+ State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP},
case fxml_stream:parse_element(Data) of
{error, _} ->
{400, [],
@@ -258,21 +259,35 @@ process(_, _) ->
%% Access verification
%% -----------------------------
-get_auth(AuthList) ->
- Admin =
- case lists:keysearch(admin, 1, AuthList) of
- {value, {admin, true}} -> true;
- _ -> false
- end,
+extract_auth(AuthList) ->
+ ?DEBUG("AUTHLIST ~p", [AuthList]),
try get_attrs([user, server, token], AuthList) of
- [U, S, T] -> {U, S, {oauth, T}, Admin}
+ [U0, S0, T] ->
+ U = jid:nodeprep(U0),
+ S = jid:nameprep(S0),
+ case ejabberd_oauth:check_token(T) of
+ {ok, {U, S}, Scope} ->
+ #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
+ {false, Reason} ->
+ {error, Reason};
+ _ ->
+ {error, not_found}
+ end
catch
exit:{attribute_not_found, _Attr, _} ->
try get_attrs([user, server, password], AuthList) of
- [U, S, P] -> {U, S, P, Admin}
+ [U0, S0, P] ->
+ U = jid:nodeprep(U0),
+ S = jid:nameprep(S0),
+ case ejabberd_auth:check_password(U, <<"">>, S, P) of
+ true ->
+ #{usr => {U, S, <<"">>}, caller_server => S};
+ false ->
+ {error, invalid_auth}
+ end
catch
- exit:{attribute_not_found, Attr, _} ->
- throw({error, missing_auth_arguments, Attr})
+ exit:{attribute_not_found, _Attr, _} ->
+ #{}
end
end.
@@ -300,12 +315,28 @@ get_auth(AuthList) ->
%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
%% {ok,{response,[152]}}
-handler(#state{get_auth = true, auth = noauth} = State,
+handler(#state{get_auth = true, auth = noauth, ip = IP} = State,
{call, Method,
[{struct, AuthList} | Arguments] = AllArgs}) ->
- try get_auth(AuthList) of
+ try extract_auth(AuthList) of
+ {error, invalid_auth} ->
+ build_fault_response(-118,
+ "Invalid authentication data",
+ []);
+ {error, not_found} ->
+ build_fault_response(-118,
+ "Invalid oauth token",
+ []);
+ {error, expired} ->
+ build_fault_response(-118,
+ "Invalid oauth token",
+ []);
+ {error, Value} ->
+ build_fault_response(-118,
+ "Invalid authentication data: ~p",
+ [Value]);
Auth ->
- handler(State#state{get_auth = false, auth = Auth},
+ handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}},
{call, Method, Arguments})
catch
{error, missing_auth_arguments, _Attr} ->
@@ -393,9 +424,14 @@ build_fault_response(Code, ParseString, ParseArgs) ->
do_command(AccessCommands, Auth, Command, AttrL, ArgsF,
ResultF) ->
ArgsFormatted = format_args(AttrL, ArgsF),
+ Auth2 = case AccessCommands of
+ V when is_list(V) ->
+ Auth#{extra_permissions => AccessCommands};
+ _ ->
+ Auth
+ end,
Result =
- ejabberd_commands:execute_command(AccessCommands, Auth,
- Command, ArgsFormatted),
+ ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2),
ResultFormatted = format_result(Result, ResultF),
{command_result, ResultFormatted}.
@@ -489,6 +525,8 @@ process_unicode_codepoints(Str) ->
format_result({error, Error}, _) ->
throw({error, Error});
+format_result({error, _Type, _Code, Error}, _) ->
+ throw({error, Error});
format_result(String, string) -> lists:flatten(String);
format_result(Atom, {Name, atom}) ->
{struct,
diff --git a/src/ext_mod.erl b/src/ext_mod.erl
index 332d2c5e2..842bb09fc 100644
--- a/src/ext_mod.erl
+++ b/src/ext_mod.erl
@@ -484,17 +484,28 @@ compile_deps(_Module, _Spec, DestDir) ->
filelib:ensure_dir(filename:join(Ebin, ".")),
Result = lists:foldl(fun(Dep, Acc) ->
Inc = filename:join(Dep, "include"),
+ Lib = filename:join(Dep, "lib"),
Src = filename:join(Dep, "src"),
Options = [{outdir, Ebin}, {i, Inc}],
[file:copy(App, Ebin) || App <- filelib:wildcard(Src++"/*.app")],
- Acc++[case compile:file(File, Options) of
+
+ %% Compile erlang files
+ Acc1 = Acc ++ [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
{ok, _, _, _} -> ok;
error -> {error, {compilation_failed, File}};
Error -> Error
end
- || File <- filelib:wildcard(Src++"/*.erl")]
+ || File <- filelib:wildcard(Src++"/*.erl")],
+
+ %% Compile elixir files
+ Acc1 ++ [case compile_elixir_file(Ebin, File) of
+ {ok, _} -> ok;
+ {error, File} -> {error, {compilation_failed, File}}
+ end
+ || File <- filelib:wildcard(Lib ++ "/*.ex")]
+
end, [], filelib:wildcard("deps/*")),
case lists:dropwhile(
fun(ok) -> true;
@@ -515,6 +526,8 @@ compile(_Module, _Spec, DestDir) ->
verbose, report_errors, report_warnings]
++ ExtLib,
[file:copy(App, Ebin) || App <- filelib:wildcard("src/*.app")],
+
+ %% Compile erlang files
Result = [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
@@ -523,14 +536,32 @@ compile(_Module, _Spec, DestDir) ->
Error -> Error
end
|| File <- filelib:wildcard("src/*.erl")],
+
+ %% Compile elixir files
+ Result1 = Result ++ [case compile_elixir_file(Ebin, File) of
+ {ok, _} -> ok;
+ {error, File} -> {error, {compilation_failed, File}}
+ end
+ || File <- filelib:wildcard("lib/*.ex")],
+
case lists:dropwhile(
fun(ok) -> true;
(_) -> false
- end, Result) of
+ end, Result1) of
[] -> ok;
[Error|_] -> Error
end.
+compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) ->
+ compile_elixir_file(list_to_binary(Dest), list_to_binary(File));
+
+compile_elixir_file(Dest, File) ->
+ try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of
+ [Module] -> {ok, Module}
+ catch
+ _ -> {error, File}
+ end.
+
install(Module, Spec, DestDir) ->
Errors = lists:dropwhile(fun({_, {ok, _}}) -> true;
(_) -> false
diff --git a/src/extauth.erl b/src/extauth.erl
index 50330b47b..6063d3670 100644
--- a/src/extauth.erl
+++ b/src/extauth.erl
@@ -102,8 +102,7 @@ call_port(Server, Msg) ->
receive {eauth, Result} -> Result end.
random_instance(MaxNum) ->
- random:seed(p1_time_compat:timestamp()),
- random:uniform(MaxNum) - 1.
+ randoms:uniform(MaxNum) - 1.
get_instances(Server) ->
ejabberd_config:get_option(
diff --git a/src/gen_mod.erl b/src/gen_mod.erl
index 476e19e9d..aaf452aeb 100644
--- a/src/gen_mod.erl
+++ b/src/gen_mod.erl
@@ -48,7 +48,7 @@
opts = [] :: opts() | '_' | '$2'}).
-type opts() :: [{atom(), any()}].
--type db_type() :: sql | mnesia | riak | ldap.
+-type db_type() :: sql | mnesia | riak.
-callback start(binary(), opts()) -> any().
-callback stop(binary()) -> any().
@@ -147,7 +147,7 @@ start_module(Host, Module) ->
-spec start_module(binary(), atom(), opts()) -> any().
start_module(Host, Module, Opts0) ->
- Opts = validate_opts(Host, Module, Opts0),
+ Opts = validate_opts(Module, Opts0),
ets:insert(ejabberd_modules,
#ejabberd_module{module_host = {Module, Host},
opts = Opts}),
@@ -308,10 +308,47 @@ get_opt_host(Host, Opts, Default) ->
Val = get_opt(host, Opts, fun iolist_to_binary/1, Default),
ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host).
-validate_opts(Host, Module, Opts) ->
+
+get_module_mod_opt_type_fun(Module) ->
+ DBSubMods = ejabberd_config:v_dbs_mods(Module),
+ fun(Opt) ->
+ Res = lists:foldl(fun(Mod, {Funs, ArgsList, _} = Acc) ->
+ case catch Mod:mod_opt_type(Opt) of
+ Fun when is_function(Fun) ->
+ {[Fun | Funs], ArgsList, true};
+ L when is_list(L) ->
+ {Funs, L ++ ArgsList, true};
+ _ ->
+ Acc
+ end
+ end, {[], [], false}, [Module | DBSubMods]),
+ case Res of
+ {[], [], false} ->
+ throw({'EXIT', {undef, mod_opt_type}});
+ {[], Args, _} -> Args;
+ {Funs, _, _} ->
+ fun(Val) ->
+ lists:any(fun(F) ->
+ try F(Val) of
+ _ ->
+ true
+ catch {replace_with, _NewVal} = E ->
+ throw(E);
+ {invalid_syntax, _Error} = E2 ->
+ throw(E2);
+ _:_ ->
+ false
+ end
+ end, Funs)
+ end
+ end
+ end.
+
+validate_opts(Module, Opts) ->
+ ModOptFun = get_module_mod_opt_type_fun(Module),
lists:filtermap(
fun({Opt, Val}) ->
- case catch validate_opt(Host, Module, Opt, Opts) of
+ case catch ModOptFun(Opt) of
VFun when is_function(VFun) ->
try VFun(Val) of
_ ->
@@ -346,22 +383,6 @@ validate_opts(Host, Module, Opts) ->
false
end, Opts).
-validate_opt(Host, Module, Opt, Opts) ->
- case Module:mod_opt_type(Opt) of
- VFun1 when is_function(VFun1) ->
- VFun1;
- L1 when is_list(L1) ->
- DBModule = db_mod(Host, Opts, Module),
- try DBModule:mod_opt_type(Opt) of
- VFun2 when is_function(VFun2) ->
- VFun2;
- L2 when is_list(L2) ->
- lists:usort(L1 ++ L2)
- catch _:undef ->
- L1
- end
- end.
-
-spec db_type(binary() | global, module()) -> db_type();
(opts(), module()) -> db_type().
@@ -378,7 +399,7 @@ db_type(Host, Module) when is_atom(Module) ->
undefined
end.
--spec db_type(global | binary(), opts(), module()) -> db_type().
+-spec db_type(binary(), opts(), module()) -> db_type().
db_type(Host, Opts, Module) ->
case catch Module:mod_opt_type(db_type) of
diff --git a/src/http_p1.erl b/src/http_p1.erl
new file mode 100644
index 000000000..f430bbe11
--- /dev/null
+++ b/src/http_p1.erl
@@ -0,0 +1,358 @@
+%%%----------------------------------------------------------------------
+%%% File : http_p1.erl
+%%% Author : Emilio Bustos <ebustos@process-one.net>
+%%% Purpose : Provide a common API for inets / lhttpc / ibrowse
+%%% Created : 29 Jul 2010 by Emilio Bustos <ebustos@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(http_p1).
+
+-author('ebustos@process-one.net').
+
+-export([start/0, stop/0, get/1, get/2, post/2, post/3,
+ request/3, request/4, request/5,
+ get_pool_size/0, set_pool_size/1]).
+
+-include("logger.hrl").
+
+-define(USE_INETS, 1).
+% -define(USE_LHTTPC, 1).
+% -define(USE_IBROWSE, 1).
+% inets used as default if none specified
+
+-ifdef(USE_IBROWSE).
+
+start() ->
+ ejabberd:start_app(ibrowse).
+
+stop() ->
+ application:stop(ibrowse).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ TimeOut = proplists:get_value(timeout, Opts, infinity),
+ Options = [{inactivity_timeout, TimeOut}
+ | proplists:delete(timeout, Opts)],
+ case ibrowse:send_req(URL, Hdrs, Method, Body, Options)
+ of
+ {ok, Status, Headers, Response} ->
+ {ok, jlib:binary_to_integer(Status), Headers,
+ Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ application:get_env(ibrowse, default_max_sessions, 10).
+
+set_pool_size(Size) ->
+ application:set_env(ibrowse, default_max_sessions, Size).
+
+-else.
+
+-ifdef(USE_LHTTPC).
+
+start() ->
+ ejabberd:start_app(lhttpc).
+
+stop() ->
+ application:stop(lhttpc).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ {[TO, SO], Rest} = proplists:split(Opts, [timeout, socket_options]),
+ TimeOut = proplists:get_value(timeout, TO, infinity),
+ SockOpt = proplists:get_value(socket_options, SO, []),
+ Options = [{connect_options, SockOpt} | Rest],
+ Result = lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options),
+ ?DEBUG("HTTP request -> response:~n"
+ "** Method = ~p~n"
+ "** URI = ~s~n"
+ "** Body = ~s~n"
+ "** Hdrs = ~p~n"
+ "** Timeout = ~p~n"
+ "** Options = ~p~n"
+ "** Response = ~p",
+ [Method, URL, Body, Hdrs, TimeOut, Options, Result]),
+ case Result of
+ {ok, {{Status, _Reason}, Headers, Response}} ->
+ {ok, Status, Headers, (Response)};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ Opts = proplists:get_value(lhttpc_manager, lhttpc_manager:list_pools()),
+ proplists:get_value(max_pool_size,Opts).
+
+set_pool_size(Size) ->
+ lhttpc_manager:set_max_pool_size(lhttpc_manager, Size).
+
+-else.
+
+start() ->
+ ejabberd:start_app(inets).
+
+stop() ->
+ application:stop(inets).
+
+to_list(Str) when is_binary(Str) ->
+ binary_to_list(Str);
+to_list(Str) ->
+ Str.
+
+request(Method, URLRaw, HdrsRaw, Body, Opts) ->
+ Hdrs = lists:map(fun({N, V}) ->
+ {to_list(N), to_list(V)}
+ end, HdrsRaw),
+ URL = to_list(URLRaw),
+
+ Request = case Method of
+ get -> {URL, Hdrs};
+ head -> {URL, Hdrs};
+ delete -> {URL, Hdrs};
+ _ -> % post, etc.
+ {URL, Hdrs,
+ to_list(proplists:get_value(<<"content-type">>, HdrsRaw, [])),
+ Body}
+ end,
+ Options = case proplists:get_value(timeout, Opts,
+ infinity)
+ of
+ infinity -> proplists:delete(timeout, Opts);
+ _ -> Opts
+ end,
+ case httpc:request(Method, Request, Options, []) of
+ {ok, {{_, Status, _}, Headers, Response}} ->
+ {ok, Status, Headers, Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ {ok, Size} = httpc:get_option(max_sessions),
+ Size.
+
+set_pool_size(Size) ->
+ httpc:set_option(max_sessions, Size).
+
+-endif.
+
+-endif.
+
+-type({header,
+ {type, 63, tuple,
+ [{type, 63, union,
+ [{type, 63, string, []}, {type, 63, atom, []}]},
+ {type, 63, string, []}]},
+ []}).
+
+-type({headers,
+ {type, 64, list, [{type, 64, header, []}]}, []}).
+
+-type({option,
+ {type, 67, union,
+ [{type, 67, tuple,
+ [{atom, 67, connect_timeout}, {type, 67, timeout, []}]},
+ {type, 68, tuple,
+ [{atom, 68, timeout}, {type, 68, timeout, []}]},
+ {type, 70, tuple,
+ [{atom, 70, send_retry},
+ {type, 70, non_neg_integer, []}]},
+ {type, 71, tuple,
+ [{atom, 71, partial_upload},
+ {type, 71, union,
+ [{type, 71, non_neg_integer, []},
+ {atom, 71, infinity}]}]},
+ {type, 72, tuple,
+ [{atom, 72, partial_download}, {type, 72, pid, []},
+ {type, 72, union,
+ [{type, 72, non_neg_integer, []},
+ {atom, 72, infinity}]}]}]},
+ []}).
+
+-type({options,
+ {type, 74, list, [{type, 74, option, []}]}, []}).
+
+-type({result,
+ {type, 76, union,
+ [{type, 76, tuple,
+ [{atom, 76, ok},
+ {type, 76, tuple,
+ [{type, 76, tuple,
+ [{type, 76, pos_integer, []}, {type, 76, string, []}]},
+ {type, 76, headers, []}, {type, 76, string, []}]}]},
+ {type, 77, tuple,
+ [{atom, 77, error}, {type, 77, atom, []}]}]},
+ []}).
+
+%% @spec (URL) -> Result
+%% URL = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, [])',
+%% that is {@link request/3} with an empty header list.
+%% @end
+%% @see request/3
+-spec get(string()) -> result().
+get(URL) -> request(get, URL, []).
+
+%% @spec (URL, Hdrs) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, Hdrs)'.
+%% @end
+%% @see request/3
+-spec get(string(), headers()) -> result().
+get(URL, Hdrs) -> request(get, URL, Hdrs).
+
+%% @spec (URL, RequestBody) -> Result
+%% URL = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request with form data.
+%% Would be the same as calling
+%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), string()) -> result().
+post(URL, Body) ->
+ request(post, URL,
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}],
+ Body).
+
+%% @spec (URL, Hdrs, RequestBody) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request.
+%% Would be the same as calling
+%% `request(post, URL, Hdrs, Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), headers(), string()) -> result().
+post(URL, Hdrs, Body) ->
+ NewHdrs = case [X
+ || {X, _} <- Hdrs,
+ str:to_lower(X) == <<"content-type">>]
+ of
+ [] ->
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}
+ | Hdrs];
+ _ -> Hdrs
+ end,
+ request(post, URL, NewHdrs, Body).
+
+%% @spec (Method, URL, Hdrs) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request without a body.
+%% Would be the same as calling `request(Method, URL, Hdrs, [], [])',
+%% that is {@link request/5} with an empty body.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers()) -> result().
+request(Method, URL, Hdrs) ->
+ request(Method, URL, Hdrs, [], []).
+
+%% @spec (Method, URL, Hdrs, RequestBody) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string()) -> result().
+request(Method, URL, Hdrs, Body) ->
+ request(Method, URL, Hdrs, Body, []).
+
+%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Options = [Option]
+%% Option = {timeout, Milliseconds | infinity} |
+%% {connect_timeout, Milliseconds | infinity} |
+%% {socket_options, [term()]} |
+
+%% Milliseconds = integer()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string(), options()) -> result().
+
+% ibrowse {response_format, response_format()} |
+% Options - [option()]
+% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result,
+% boolean()} | {headers_as_is, boolean()}
+%body_format() = string() | binary()
+% The body_format option is only valid for the synchronous request and the default is string.
+% When making an asynchronous request the body will always be received as a binary.
+% lhttpc: always binary
+
diff --git a/src/jid.erl b/src/jid.erl
index 9e8ea9d23..287a0642b 100644
--- a/src/jid.erl
+++ b/src/jid.erl
@@ -52,11 +52,35 @@
-spec start() -> ok.
start() ->
+ {ok, Owner} = ets_owner(),
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
- catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
+ %% Table is public to allow ETS insert to fix / update the table even if table already exist
+ %% with another owner.
+ catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
ok.
+ets_owner() ->
+ case whereis(jlib_ets) of
+ undefined ->
+ Pid = spawn(fun() -> ets_keepalive() end),
+ case catch register(jlib_ets, Pid) of
+ true ->
+ {ok, Pid};
+ Error -> Error
+ end;
+ Pid ->
+ {ok,Pid}
+ end.
+
+%% Process used to keep jlib ETS table alive in case the original owner dies.
+%% The table need to be public, otherwise subsequent inserts would fail.
+ets_keepalive() ->
+ receive
+ _ ->
+ ets_keepalive()
+ end.
+
-spec make(binary(), binary(), binary()) -> jid() | error.
make(User, Server, Resource) ->
diff --git a/src/jlib.erl b/src/jlib.erl
index b79b8fa7c..aca3b0ee8 100644
--- a/src/jlib.erl
+++ b/src/jlib.erl
@@ -373,15 +373,20 @@ iq_type_to_string(error) -> <<"error">>.
-spec iq_to_xml(IQ :: iq()) -> xmlel().
iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
+ Children =
+ if
+ is_list(SubEl) -> SubEl;
+ true -> [SubEl]
+ end,
if ID /= <<"">> ->
#xmlel{name = <<"iq">>,
attrs =
[{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}],
- children = SubEl};
+ children = Children};
true ->
#xmlel{name = <<"iq">>,
attrs = [{<<"type">>, iq_type_to_string(Type)}],
- children = SubEl}
+ children = Children}
end.
-spec parse_xdata_submit(El :: xmlel()) ->
@@ -579,33 +584,8 @@ add_delay_info(El, From, Time) ->
binary()) -> xmlel().
add_delay_info(El, From, Time, Desc) ->
- case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of
- false ->
- %% Add new tag
DelayTag = create_delay_tag(Time, From, Desc),
- fxml:append_subtags(El, [DelayTag]);
- DelayTag ->
- %% Update existing tag
- NewDelayTag =
- case {fxml:get_tag_cdata(DelayTag), Desc} of
- {<<"">>, <<"">>} ->
- DelayTag;
- {OldDesc, <<"">>} ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]};
- {<<"">>, NewDesc} ->
- DelayTag#xmlel{children = [{xmlcdata, NewDesc}]};
- {OldDesc, NewDesc} ->
- case binary:match(OldDesc, NewDesc) of
- nomatch ->
- FinalDesc = <<OldDesc/binary, ", ", NewDesc/binary>>,
- DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]};
- _ ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}
- end
- end,
- NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}),
- fxml:append_subtags(NewEl, [NewDelayTag])
- end.
+ fxml:append_subtags(El, [DelayTag]).
-spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary())
-> xmlel() | error.
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index 4598805c2..69fffbd7c 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -378,6 +378,7 @@ get_commands_spec() ->
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
+ longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"",
module = ?MODULE, function = add_rosteritem,
args = [{localuser, binary}, {localserver, binary},
{user, binary}, {server, binary},
@@ -536,7 +537,7 @@ get_commands_spec() ->
policy = user,
module = mod_offline, function = count_offline_messages,
args = [],
- result = {res, integer}},
+ result = {value, integer}},
#ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID",
module = ?MODULE, function = send_message,
@@ -864,12 +865,15 @@ connected_users_vhost(Host) ->
%% Code copied from ejabberd_sm.erl and customized
dirty_get_sessions_list2() ->
- mnesia:dirty_select(
+ Ss = mnesia:dirty_select(
session,
- [{#session{usr = '$1', sid = {'$2', '$3'}, priority = '$4', info = '$5',
+ [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4',
_ = '_'},
- [{is_pid, '$3'}],
- [['$1', {{'$2', '$3'}}, '$4', '$5']]}]).
+ [],
+ [['$1', '$2', '$3', '$4']]}]),
+ lists:filter(fun([_USR, _SID, _Priority, Info]) ->
+ not proplists:get_bool(offline, Info)
+ end, Ss).
%% Make string more print-friendly
stringize(String) ->
@@ -906,8 +910,8 @@ user_sessions_info(User, Host) ->
{'EXIT', _Reason} ->
[];
Ss ->
- lists:filter(fun(#session{sid = {_, Pid}}) ->
- is_pid(Pid)
+ lists:filter(fun(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info)
end, Ss)
end,
lists:map(
@@ -1140,8 +1144,8 @@ subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -
subscribe_roster({Name, Server, Group, Nick}, Roster);
%% Subscribe Name2 to Name1
subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) ->
- subscribe(Name1, Server1, list_to_binary(Name2), list_to_binary(Server2),
- list_to_binary(Nick2), list_to_binary(Group2), <<"both">>, []),
+ subscribe(Name1, Server1, iolist_to_binary(Name2), iolist_to_binary(Server2),
+ iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []),
subscribe_roster({Name1, Server1, Group1, Nick1}, Roster).
push_alltoall(S, G) ->
@@ -1173,10 +1177,11 @@ push_roster_item(LU, LS, R, U, S, Action) ->
ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ).
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
+ Groups = binary:split(Group,<<";">>, [global]),
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = jlib:binary_to_atom(Subs),
- groups = [Group]};
+ groups = Groups};
build_roster_item(U, S, remove) ->
#roster_item{jid = jid:make(U, S), subscription = remove}.
@@ -1260,11 +1265,11 @@ srg_create(Group, Host, Name, Description, Display) ->
Opts = [{name, Name},
{displayed_groups, DisplayList},
{description, Description}],
- {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts),
+ {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_delete(Group, Host) ->
- {atomic, ok} = mod_shared_roster:delete_group(Host, Group),
+ {atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
srg_list(Host) ->
@@ -1287,11 +1292,11 @@ srg_get_members(Group, Host) ->
|| {MUser, MServer} <- Members].
srg_user_add(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
ok.
srg_user_del(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
ok.
@@ -1302,44 +1307,9 @@ srg_user_del(User, Host, Group, GroupHost) ->
%% @doc Send a message to a Jabber account.
%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok
send_message(Type, From, To, Subject, Body) ->
+ FromJID = jid:from_string(From),
+ ToJID = jid:from_string(To),
Packet = build_packet(Type, Subject, Body),
- send_packet_all_resources(From, To, Packet).
-
-%% @doc Send a packet to a Jabber account.
-%% If a resource was specified in the JID,
-%% the packet is sent only to that specific resource.
-%% If no resource was specified in the JID,
-%% and the user is remote or local but offline,
-%% the packet is sent to the bare JID.
-%% If the user is local and is online in several resources,
-%% the packet is sent to all its resources.
-send_packet_all_resources(FromJIDString, ToJIDString, Packet) ->
- FromJID = jid:from_string(FromJIDString),
- ToJID = jid:from_string(ToJIDString),
- ToUser = ToJID#jid.user,
- ToServer = ToJID#jid.server,
- case ToJID#jid.resource of
- <<>> ->
- send_packet_all_resources(FromJID, ToUser, ToServer, Packet);
- Res ->
- send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet)
- end.
-
-send_packet_all_resources(FromJID, ToUser, ToServer, Packet) ->
- case ejabberd_sm:get_user_resources(ToUser, ToServer) of
- [] ->
- send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet);
- ToResources ->
- lists:foreach(
- fun(ToResource) ->
- send_packet_all_resources(FromJID, ToUser, ToServer,
- ToResource, Packet)
- end,
- ToResources)
- end.
-
-send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) ->
- ToJID = jid:make(ToU, ToS, ToR),
ejabberd_router:route(FromJID, ToJID, Packet).
build_packet(Type, Subject, Body) ->
diff --git a/src/mod_announce.erl b/src/mod_announce.erl
index 495cbf946..1d93cbe65 100644
--- a/src/mod_announce.erl
+++ b/src/mod_announce.erl
@@ -609,8 +609,8 @@ announce_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
- Dest = jid:make(User, Server),
- ejabberd_router:route(Local, Dest, Packet)
+ Dest = jid:make(User, Server, <<>>),
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:get_vh_registered_users(Host))
end.
@@ -626,8 +626,8 @@ announce_all_hosts_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
- Dest = jid:make(User, Server),
- ejabberd_router:route(Local, Dest, Packet)
+ Dest = jid:make(User, Server, <<>>),
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:dirty_get_registered_users())
end.
@@ -813,7 +813,7 @@ send_announcement_to_all(Host, SubjectS, BodyS) ->
lists:foreach(
fun({U, S, R}) ->
Dest = jid:make(U, S, R),
- ejabberd_router:route(Local, Dest, Packet)
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, Sessions).
-spec get_access(global | binary()) -> atom().
@@ -823,6 +823,10 @@ get_access(Host) ->
fun(A) -> A end,
none).
+-spec add_store_hint(stanza()) -> stanza().
+add_store_hint(El) ->
+ xmpp:set_subtag(El, #hint{type = store}).
+
%%-------------------------------------------------------------------------
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl
index e35caa1c7..023e8dc6f 100644
--- a/src/mod_carboncopy.erl
+++ b/src/mod_carboncopy.erl
@@ -123,6 +123,7 @@ user_receive_packet(Packet, _C2SState, JID, _From, To) ->
stanza() | {stop, stanza()}.
check_and_forward(JID, To, Packet, Direction)->
case is_chat_message(Packet) andalso
+ not is_muc_pm(To, Packet) andalso
xmpp:has_subtag(Packet, #carbons_private{}) == false andalso
xmpp:has_subtag(Packet, #hint{type = 'no-copy'}) == false of
true ->
@@ -232,6 +233,11 @@ is_chat_message(#message{type = normal, body = Body}) ->
is_chat_message(_) ->
false.
+is_muc_pm(#jid{lresource = <<>>}, _Packet) ->
+ false;
+is_muc_pm(_To, Packet) ->
+ xmpp:has_subtag(Packet, #muc_user{}).
+
-spec list(binary(), binary()) -> [{binary(), binary()}].
%% list {resource, cc_version} with carbons enabled for given user and host
list(User, Server) ->
diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl
index 7f0658eff..2bae7a4f8 100644
--- a/src/mod_client_state.erl
+++ b/src/mod_client_state.erl
@@ -34,8 +34,8 @@
-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% ejabberd_hooks callbacks.
--export([filter_presence/3, filter_chat_states/3, filter_pep/3, filter_other/3,
- flush_queue/2, add_stream_feature/2]).
+-export([filter_presence/4, filter_chat_states/4, filter_pep/4, filter_other/4,
+ flush_queue/3, add_stream_feature/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -151,26 +151,27 @@ depends(_Host, _Opts) ->
%% ejabberd_hooks callbacks.
%%--------------------------------------------------------------------
--spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_presence({C2SState, _OutStanzas} = Acc, Host,
+filter_presence({C2SState, _OutStanzas} = Acc, Host, To,
#presence{type = Type} = Stanza) ->
if Type == available; Type == unavailable ->
- ?DEBUG("Got availability presence stanza", []),
+ ?DEBUG("Got availability presence stanza for ~s",
+ [jid:to_string(To)]),
queue_add(presence, Stanza, Host, C2SState);
true ->
Acc
end;
-filter_presence(Acc, _Host, _Stanza) -> Acc.
+filter_presence(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
- #message{from = From, to = To} = Stanza) ->
+filter_chat_states({C2SState, _OutStanzas} = Acc, Host, To,
+ #message{from = From} = Stanza) ->
case xmpp_util:is_standalone_chat_state(Stanza) of
true ->
case {From, To} of
@@ -180,40 +181,41 @@ filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
%% conversations across clients.
Acc;
_ ->
- ?DEBUG("Got standalone chat state notification", []),
+ ?DEBUG("Got standalone chat state notification for ~s",
+ [jid:to_string(To)]),
queue_add(chatstate, Stanza, Host, C2SState)
end;
false ->
Acc
end;
-filter_chat_states(Acc, _Host, _Stanza) -> Acc.
+filter_chat_states(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_pep({C2SState, _OutStanzas} = Acc, Host, #message{} = Stanza) ->
+filter_pep({C2SState, _OutStanzas} = Acc, Host, To, #message{} = Stanza) ->
case get_pep_node(Stanza) of
undefined ->
Acc;
Node ->
- ?DEBUG("Got PEP notification", []),
+ ?DEBUG("Got PEP notification for ~s", [jid:to_string(To)]),
queue_add({pep, Node}, Stanza, Host, C2SState)
end;
-filter_pep(Acc, _Host, _Stanza) -> Acc.
+filter_pep(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
- -> {stop, {ejabberd_c2s:state(), [stanza()]}}.
+-spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
+ -> {ejabberd_c2s:state(), [stanza()]}.
-filter_other({C2SState, _OutStanzas}, Host, Stanza) ->
- ?DEBUG("Won't add stanza to CSI queue", []),
+filter_other({C2SState, _OutStanzas}, Host, To, Stanza) ->
+ ?DEBUG("Won't add stanza for ~s to CSI queue", [jid:to_string(To)]),
queue_take(Stanza, Host, C2SState).
--spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary())
+-spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary(), jid())
-> {ejabberd_c2s:state(), [stanza()]}.
-flush_queue({C2SState, _OutStanzas}, Host) ->
- ?DEBUG("Going to flush CSI queue", []),
+flush_queue({C2SState, _OutStanzas}, Host, JID) ->
+ ?DEBUG("Going to flush CSI queue of ~s", [jid:to_string(JID)]),
Queue = get_queue(C2SState),
NewState = set_queue([], C2SState),
{NewState, get_stanzas(Queue, Host)}.
@@ -246,7 +248,7 @@ queue_add(Type, Stanza, Host, C2SState) ->
{stop, {NewState, []}}
end.
--spec queue_take(stanza(), binary(), term()) -> {stop, {term(), [stanza()]}}.
+-spec queue_take(stanza(), binary(), term()) -> {term(), [stanza()]}.
queue_take(Stanza, Host, C2SState) ->
From = xmpp:get_from(Stanza),
@@ -256,7 +258,7 @@ queue_take(Stanza, Host, C2SState) ->
U == LUser andalso S == LServer
end, get_queue(C2SState)),
NewState = set_queue(Rest, C2SState),
- {stop, {NewState, get_stanzas(Selected, Host) ++ [Stanza]}}.
+ {NewState, get_stanzas(Selected, Host) ++ [Stanza]}.
-spec set_queue(csi_queue(), term()) -> term().
diff --git a/src/mod_configure.erl b/src/mod_configure.erl
index 5e2ff351c..fc274dc03 100644
--- a/src/mod_configure.erl
+++ b/src/mod_configure.erl
@@ -1084,7 +1084,7 @@ get_form(Host, [<<"config">>, <<"acls">>], Lang) ->
ACLs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
- [ets:select(
+ [mnesia:dirty_select(
acl,
ets:fun2ms(
fun({acl, {Name, H}, Spec}) when H == Host ->
@@ -1103,7 +1103,7 @@ get_form(Host, [<<"config">>, <<"access">>], Lang) ->
Accs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
- [ets:select(
+ [mnesia:dirty_select(
access,
ets:fun2ms(
fun({access, {Name, H}, Acc}) when H == Host ->
@@ -1568,21 +1568,29 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
Xmlelement = xmpp:serr_policy_violation(<<"has been kicked">>, Lang),
case JID#jid.lresource of
<<>> ->
- SIDs = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, '_'},
+ SIs = mnesia:dirty_select(session,
+ [{#session{usr = {LUser, LServer, '_'},
+ sid = '$1',
+ info = '$2',
_ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- [Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs];
+ [], [{{'$1', '$2'}}]}]),
+ Pids = [P || {{_, P}, Info} <- SIs,
+ not proplists:get_bool(offline, Info)],
+ lists:foreach(fun(Pid) ->
+ Pid ! {kick, kicked_by_admin, Xmlelement}
+ end, Pids);
R ->
- [{_, Pid}] = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, R},
+ [{{_, Pid}, Info}] = mnesia:dirty_select(
+ session,
+ [{#session{usr = {LUser, LServer, R},
+ sid = '$1',
+ info = '$2',
_ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- Pid ! {kick, kicked_by_admin, Xmlelement}
+ [], [{{'$1', '$2'}}]}]),
+ case proplists:get_bool(offline, Info) of
+ true -> ok;
+ false -> Pid ! {kick, kicked_by_admin, Xmlelement}
+ end
end,
{result, undefined};
set_form(From, Host,
diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl
new file mode 100644
index 000000000..7fec01dcb
--- /dev/null
+++ b/src/mod_delegation.erl
@@ -0,0 +1,325 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 10 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_delegation).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+-export([component_connected/1, component_disconnected/2,
+ ejabberd_local/1, ejabberd_sm/1,
+ disco_local_features/5, disco_sm_features/5,
+ disco_local_identity/5, disco_sm_identity/5]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("xmpp.hrl").
+
+-type disco_acc() :: {error, stanza_error()} | {result, [binary()]} | empty.
+-record(state, {server_host = <<"">> :: binary(),
+ delegations = dict:new() :: ?TDICT}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(ejabberd_sup, PingSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
+mod_opt_type(namespaces) -> validate_fun();
+mod_opt_type(_) ->
+ [namespaces, iqdisc].
+
+depends(_, _) ->
+ [].
+
+-spec component_connected(binary()) -> ok.
+component_connected(Host) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_connected, Host})
+ end, ?MYHOSTS).
+
+-spec component_disconnected(binary(), binary()) -> ok.
+component_disconnected(Host, _Reason) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_disconnected, Host})
+ end, ?MYHOSTS).
+
+-spec ejabberd_local(iq()) -> iq().
+ejabberd_local(IQ) ->
+ process_iq(IQ, ejabberd_local).
+
+-spec ejabberd_sm(iq()) -> iq().
+ejabberd_sm(IQ) ->
+ process_iq(IQ, ejabberd_sm).
+
+-spec disco_local_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_local_features(Acc, From, To, Node, Lang) ->
+ disco_features(Acc, From, To, Node, Lang, ejabberd_local).
+
+-spec disco_sm_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_sm_features(Acc, From, To, Node, Lang) ->
+ disco_features(Acc, From, To, Node, Lang, ejabberd_sm).
+
+-spec disco_local_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_local_identity(Acc, From, To, Node, Lang) ->
+ disco_identity(Acc, From, To, Node, Lang, ejabberd_local).
+
+-spec disco_sm_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_sm_identity(Acc, From, To, Node, Lang) ->
+ disco_identity(Acc, From, To, Node, Lang, ejabberd_sm).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([Host, _Opts]) ->
+ ejabberd_hooks:add(component_connected, ?MODULE,
+ component_connected, 50),
+ ejabberd_hooks:add(component_disconnected, ?MODULE,
+ component_disconnected, 50),
+ ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
+ disco_local_features, 50),
+ ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
+ disco_sm_features, 50),
+ ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
+ disco_local_identity, 50),
+ ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE,
+ disco_sm_identity, 50),
+ {ok, #state{server_host = Host}}.
+
+handle_call(get_delegations, _From, State) ->
+ {reply, {ok, State#state.delegations}, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({component_connected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ To = jid:make(Host),
+ NSAttrsAccessList = gen_mod:get_module_opt(
+ ServerHost, ?MODULE, namespaces,
+ validate_fun(), []),
+ lists:foreach(
+ fun({NS, _Attrs, Access}) ->
+ case acl:match_rule(ServerHost, Access, To) of
+ allow ->
+ send_disco_queries(ServerHost, Host, NS);
+ deny ->
+ ok
+ end
+ end, NSAttrsAccessList),
+ {noreply, State};
+handle_cast({disco_info, Type, Host, NS, Info}, State) ->
+ From = jid:make(State#state.server_host),
+ To = jid:make(Host),
+ case dict:find({NS, Type}, State#state.delegations) of
+ error ->
+ Msg = #message{from = From, to = To,
+ sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]},
+ Delegations = dict:store({NS, Type}, {Host, Info}, State#state.delegations),
+ gen_iq_handler:add_iq_handler(Type, State#state.server_host, NS,
+ ?MODULE, Type, one_queue),
+ ejabberd_router:route(From, To, Msg),
+ ?INFO_MSG("Namespace '~s' is delegated to external component '~s'",
+ [NS, Host]),
+ {noreply, State#state{delegations = Delegations}};
+ {ok, {AnotherHost, _}} ->
+ ?WARNING_MSG("Failed to delegate namespace '~s' to "
+ "external component '~s' because it's already "
+ "delegated to '~s'",
+ [NS, Host, AnotherHost]),
+ {noreply, State}
+ end;
+handle_cast({component_disconnected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ Delegations =
+ dict:filter(
+ fun({NS, Type}, {H, _}) when H == Host ->
+ ?INFO_MSG("Remove delegation of namespace '~s' "
+ "from external component '~s'",
+ [NS, Host]),
+ gen_iq_handler:remove_iq_handler(Type, ServerHost, NS),
+ false;
+ (_, _) ->
+ true
+ end, State#state.delegations),
+ {noreply, State#state{delegations = Delegations}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ %% Note: we don't remove component_* hooks because they are global
+ %% and might be registered within a module on another virtual host
+ ServerHost = State#state.server_host,
+ ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE,
+ disco_local_features, 50),
+ ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE,
+ disco_sm_features, 50),
+ ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE,
+ disco_local_identity, 50),
+ ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE,
+ disco_sm_identity, 50),
+ lists:foreach(
+ fun({NS, Type}) ->
+ gen_iq_handler:remove_iq_handler(Type, ServerHost, NS)
+ end, dict:fetch_keys(State#state.delegations)).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+-spec get_delegations(binary()) -> ?TDICT.
+get_delegations(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ try gen_server:call(Proc, get_delegations) of
+ {ok, Delegations} -> Delegations
+ catch exit:{noproc, _} ->
+ %% No module is loaded for this virtual host
+ dict:new()
+ end.
+
+-spec process_iq(iq(), ejabberd_local | ejabberd_sm) -> ignore | iq().
+process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) ->
+ LServer = To#jid.lserver,
+ NS = xmpp:get_ns(SubEl),
+ Delegations = get_delegations(LServer),
+ case dict:find({NS, Type}, Delegations) of
+ {ok, {Host, _}} ->
+ Delegation = #delegation{forwarded = #forwarded{sub_els = [IQ]}},
+ NewFrom = jid:make(LServer),
+ NewTo = jid:make(Host),
+ ejabberd_local:route_iq(
+ NewFrom, NewTo,
+ #iq{type = set,
+ from = NewFrom,
+ to = NewTo,
+ sub_els = [Delegation]},
+ fun(Result) -> process_iq_result(IQ, Result) end),
+ ignore;
+ error ->
+ Txt = <<"Failed to map delegated namespace to external component">>,
+ xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
+ end.
+
+-spec process_iq_result(iq(), iq()) -> ok.
+process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ,
+ #iq{type = result} = ResIQ) ->
+ case xmpp:get_subtag(ResIQ, #delegation{}) of
+ #delegation{
+ forwarded = #forwarded{
+ sub_els = [#iq{from = To, to = From,
+ type = Type, id = ID} = Reply]}}
+ when Type == error; Type == result ->
+ ejabberd_router:route(From, To, Reply);
+ _ ->
+ ?ERROR_MSG("got iq-result with invalid delegated "
+ "payload:~n~s", [xmpp:pp(ResIQ)]),
+ Txt = <<"External component failure">>,
+ Err = xmpp:err_internal_server_error(Txt, Lang),
+ ejabberd_router:route_error(To, From, IQ, Err)
+ end;
+process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) ->
+ Err = xmpp:set_from_to(ResIQ, To, From),
+ ejabberd_router:route(To, From, Err);
+process_iq_result(#iq{from = From, to = To, lang = Lang} = IQ, timeout) ->
+ Txt = <<"External component timeout">>,
+ Err = xmpp:err_internal_server_error(Txt, Lang),
+ ejabberd_router:route_error(To, From, IQ, Err).
+
+-spec send_disco_queries(binary(), binary(), binary()) -> ok.
+send_disco_queries(LServer, Host, NS) ->
+ From = jid:make(LServer),
+ To = jid:make(Host),
+ lists:foreach(
+ fun({Type, Node}) ->
+ ejabberd_local:route_iq(
+ From, To, #iq{type = get, from = From, to = To,
+ sub_els = [#disco_info{node = Node}]},
+ fun(#iq{type = result, sub_els = [#disco_info{} = Info]}) ->
+ Proc = gen_mod:get_module_proc(LServer, ?MODULE),
+ gen_server:cast(Proc, {disco_info, Type, Host, NS, Info});
+ (_) ->
+ ok
+ end)
+ end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>},
+ {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]).
+
+-spec disco_features(disco_acc(), jid(), jid(), binary(), binary(),
+ ejabberd_local | ejabberd_sm) -> disco_acc().
+disco_features(Acc, _From, To, <<"">>, _Lang, Type) ->
+ Delegations = get_delegations(To#jid.lserver),
+ Features = my_features(Type) ++
+ lists:flatmap(
+ fun({{_, T}, {_, Info}}) when T == Type ->
+ Info#disco_info.features;
+ (_) ->
+ []
+ end, dict:to_list(Delegations)),
+ case Acc of
+ empty when Features /= [] -> {result, Features};
+ {result, Fs} -> {result, Fs ++ Features};
+ _ -> Acc
+ end;
+disco_features(Acc, _, _, _, _, _) ->
+ Acc.
+
+-spec disco_identity(disco_acc(), jid(), jid(), binary(), binary(),
+ ejabberd_local | ejabberd_sm) -> disco_acc().
+disco_identity(Acc, _From, To, <<"">>, _Lang, Type) ->
+ Delegations = get_delegations(To#jid.lserver),
+ Identities = lists:flatmap(
+ fun({{_, T}, {_, Info}}) when T == Type ->
+ Info#disco_info.identities;
+ (_) ->
+ []
+ end, dict:to_list(Delegations)),
+ case Acc of
+ empty when Identities /= [] -> {result, Identities};
+ {result, Ids} -> {result, Ids ++ Identities};
+ Acc -> Acc
+ end.
+
+my_features(ejabberd_local) -> [?NS_DELEGATION];
+my_features(ejabberd_sm) -> [].
+
+validate_fun() ->
+ fun(L) ->
+ lists:map(
+ fun({NS, Opts}) ->
+ Attrs = proplists:get_value(filtering, Opts, []),
+ Access = proplists:get_value(access, Opts, none),
+ {NS, Attrs, Access}
+ end, L)
+ end.
diff --git a/src/mod_echo.erl b/src/mod_echo.erl
index fe4b8d90d..e7d64dd67 100644
--- a/src/mod_echo.erl
+++ b/src/mod_echo.erl
@@ -63,7 +63,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 1578be964..881587ede 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -101,7 +101,7 @@
-define(AC_ALLOW_HEADERS,
{<<"Access-Control-Allow-Headers">>,
- <<"Content-Type">>}).
+ <<"Content-Type, Authorization, X-Admin">>}).
-define(AC_MAX_AGE,
{<<"Access-Control-Max-Age">>, <<"86400">>}).
@@ -118,9 +118,11 @@
%% -------------------
start(_Host, _Opts) ->
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
ok.
stop(_Host) ->
+ ejabberd_access_permissions:unregister_permission_addon(?MODULE),
ok.
depends(_Host, _Opts) ->
@@ -130,79 +132,39 @@ depends(_Host, _Opts) ->
%% basic auth
%% ----------
-check_permissions(Request, Command) ->
- case catch binary_to_existing_atom(Command, utf8) of
- Call when is_atom(Call) ->
- {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
- check_permissions2(Request, Call, CommandPolicy);
- _ ->
- unauthorized_response()
- end.
-
-check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
- when HTTPAuth /= undefined ->
- Admin =
- case lists:keysearch(<<"X-Admin">>, 1, Headers) of
- {value, {_, <<"true">>}} -> true;
- _ -> false
- end,
- Auth =
- case HTTPAuth of
+extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
+ Info = case HTTPAuth of
{SJID, Pass} ->
case jid:from_string(SJID) of
- #jid{user = User, server = Server} ->
+ #jid{luser = User, lserver = Server} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
- true -> {ok, {User, Server, Pass, Admin}};
- false -> false
+ true ->
+ #{usr => {User, Server, <<"">>}, caller_server => Server};
+ false ->
+ {error, invalid_auth}
end;
_ ->
- false
+ {error, invalid_auth}
end;
{oauth, Token, _} ->
- case oauth_check_token(Call, Token) of
- {ok, user, {User, Server}} ->
- {ok, {User, Server, {oauth, Token}, Admin}};
- {ok, server_admin} -> %% token whas generated using issue_token command line
- {ok, admin};
- false ->
- false
+ case ejabberd_oauth:check_token(Token) of
+ {ok, {U, S}, Scope} ->
+ #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
+ {false, Reason} ->
+ {error, Reason}
end;
_ ->
- false
+ #{}
end,
- case Auth of
- {ok, A} -> {allowed, Call, A};
- _ -> unauthorized_response()
- end;
-check_permissions2(_Request, Call, open) ->
- {allowed, Call, noauth};
-check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
- Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
- fun(V) -> V end,
- none),
- Res = acl:match_rule(global, Access, IP),
- case Res of
- all ->
- {allowed, Call, admin};
- [all] ->
- {allowed, Call, admin};
- allow ->
- {allowed, Call, admin};
- Commands when is_list(Commands) ->
- case lists:member(Call, Commands) of
- true -> {allowed, Call, admin};
- _ -> unauthorized_response()
- end;
- _E ->
- {allowed, Call, noauth}
+ case Info of
+ Map when is_map(Map) ->
+ Map#{caller_module => ?MODULE, ip => IP};
+ _ ->
+ ?DEBUG("Invalid auth data: ~p", [Info]),
+ Info
end;
-check_permissions2(_Request, _Call, _Policy) ->
- unauthorized_response().
-
-oauth_check_token(Scope, Token) when is_atom(Scope) ->
- oauth_check_token(atom_to_binary(Scope, utf8), Token);
-oauth_check_token(Scope, Token) ->
- ejabberd_oauth:check_token(Scope, Token).
+extract_auth(#request{ip = IP}) ->
+ #{ip => IP, caller_module => ?MODULE}.
%% ------------------
%% command processing
@@ -213,31 +175,24 @@ oauth_check_token(Scope, Token) ->
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response(<<"Missing POST data">>);
-process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
+process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
Version = get_api_version(Req),
try
- Args = case jiffy:decode(Data) of
- List when is_list(List) -> List;
- {List} when is_list(List) -> List;
- Other -> [Other]
- end,
+ Args = extract_args(Data),
log(Call, Args, IPPort),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args, Version, IP),
- json_response(Code, jiffy:encode(Result));
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
- catch _:{error,{_,invalid_json}} = _Err ->
+ perform_call(Call, Args, Req, Version)
+ catch
+ %% TODO We need to refactor to remove redundant error return formatting
+ throw:{error, unknown_command} ->
+ json_format({404, 44, <<"Command not found.">>});
+ _:{error,{_,invalid_json}} = _Err ->
?DEBUG("Bad Request: ~p", [_Err]),
badrequest_response(<<"Invalid JSON input">>);
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
+process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
Version = get_api_version(Req),
try
Args = case Data of
@@ -245,23 +200,48 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
_ -> Data
end,
log(Call, Args, IP),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args, Version, IP),
- json_response(Code, jiffy:encode(Result));
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
- catch _:_Error ->
+ perform_call(Call, Args, Req, Version)
+ catch
+ %% TODO We need to refactor to remove redundant error return formatting
+ throw:{error, unknown_command} ->
+ json_format({404, 44, <<"Command not found.">>});
+ _:_Error ->
+
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([], #request{method = 'OPTIONS', data = <<>>}) ->
+process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []};
+process(_, #request{method = 'OPTIONS'}) ->
+ {400, ?OPTIONS_HEADER, []};
process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
- badrequest_response().
+ json_error(400, 40, <<"Missing command name.">>).
+
+perform_call(Command, Args, Req, Version) ->
+ case catch binary_to_existing_atom(Command, utf8) of
+ Call when is_atom(Call) ->
+ case extract_auth(Req) of
+ {error, expired} -> invalid_token_response();
+ {error, not_found} -> invalid_token_response();
+ {error, invalid_auth} -> unauthorized_response();
+ {error, _} -> unauthorized_response();
+ Auth when is_map(Auth) ->
+ Result = handle(Call, Auth, Args, Version),
+ json_format(Result)
+ end;
+ _ ->
+ json_error(404, 40, <<"Endpoint not found.">>)
+ end.
+
+%% Be tolerant to make API more easily usable from command-line pipe.
+extract_args(<<"\n">>) -> [];
+extract_args(Data) ->
+ case jiffy:decode(Data) of
+ List when is_list(List) -> List;
+ {List} when is_list(List) -> List;
+ Other -> [Other]
+ end.
% get API version N from last "vN" element in URL path
get_api_version(#request{path = Path}) ->
@@ -282,8 +262,10 @@ get_api_version([]) ->
%% command handlers
%% ----------------
+%% TODO Check accept types of request before decided format of reply.
+
% generic ejabberd command handler
-handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
+handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
@@ -300,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
try
- handle2(Call, Auth, match(Args2, Spec), Version, IP)
+ handle2(Call, Auth, match(Args2, Spec), Version)
catch throw:not_found ->
{404, <<"not_found">>};
throw:{not_found, Why} when is_atom(Why) ->
@@ -314,7 +296,9 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
throw:{not_allowed, Msg} ->
{401, iolist_to_binary(Msg)};
throw:{error, account_unprivileged} ->
- {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
+ {403, 31, <<"Command need to be run with admin priviledge.">>};
+ throw:{error, access_rules_unauthorized} ->
+ {403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>};
throw:{invalid_parameter, Msg} ->
{400, iolist_to_binary(Msg)};
throw:{error, Why} when is_atom(Why) ->
@@ -337,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{400, <<"Error">>}
end.
-handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
+handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
- ejabberd_command(Auth, Call, ArgsFormatted, Version, IP).
+ case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
+ {error, Error} ->
+ throw(Error);
+ Res ->
+ format_command_result(Call, Auth, Res, Version)
+ end.
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
@@ -370,28 +359,47 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
-format_arg({array, Elements},
- {list, {ElementDefName, ElementDefFormat}})
+format_arg({Elements},
+ {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
+ when is_list(Elements) andalso
+ (Tuple1S == binary orelse Tuple1S == string) ->
+ lists:map(fun({F1, F2}) ->
+ {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
+ ({Val}) when is_list(Val) ->
+ format_arg({Val}, Tuple)
+ end, Elements);
+format_arg(Elements,
+ {list, {_ElementDefName, {list, _} = ElementDefFormat}})
when is_list(Elements) ->
- lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
- ElementDefName == ElementName ->
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
- {list, {ElementDefName, ElementDefFormat}})
+ [{format_arg(Element, ElementDefFormat)}
+ || Element <- Elements];
+format_arg(Elements,
+ {list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
- lists:map(fun ({ElementName, ElementValue}) ->
- true = ElementDefName == ElementName,
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
+ [format_arg(Element, ElementDefFormat)
+ || Element <- Elements];
+format_arg({[{Name, Value}]},
+ {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
+ when Tuple1S == binary;
+ Tuple1S == string ->
+ {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
+format_arg({Elements},
{tuple, ElementsDef})
when is_list(Elements) ->
- FormattedList = format_args(Elements, ElementsDef),
- list_to_tuple(FormattedList);
-format_arg({array, Elements}, {list, ElementsDef})
+ F = lists:map(fun({TElName, TElDef}) ->
+ case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
+ {_, Value} ->
+ format_arg(Value, TElDef);
+ _ when TElDef == binary; TElDef == string ->
+ <<"">>;
+ _ ->
+ ?ERROR_MSG("missing field ~p in tuple ~p", [TElName, Elements]),
+ throw({invalid_parameter,
+ io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
+ end
+ end, ElementsDef),
+ list_to_tuple(F);
+format_arg(Elements, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
|| Element <- Elements];
@@ -405,7 +413,7 @@ format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
throw({invalid_parameter,
- io_lib:format("Arg ~p is not in format ~p",
+ io_lib:format("Arg ~w is not in format ~w",
[Arg, Format])}).
process_unicode_codepoints(Str) ->
@@ -420,18 +428,6 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
-ejabberd_command(Auth, Cmd, Args, Version, IP) ->
- Access = case Auth of
- admin -> [];
- _ -> undefined
- end,
- case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of
- {error, Error} ->
- throw(Error);
- Res ->
- format_command_result(Cmd, Auth, Res, Version)
- end.
-
format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
@@ -439,10 +435,12 @@ format_command_result(Cmd, Auth, Result, Version) ->
{200, 0};
{{_, rescode}, _} ->
{200, 1};
- {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
- {200, iolist_to_binary(Text1)};
- {{_, restuple}, {_, Text2}} ->
- {500, iolist_to_binary(Text2)};
+ {_, {error, ErrorAtom, Code, Msg}} ->
+ format_error_result(ErrorAtom, Code, Msg);
+ {{_, restuple}, {V, Text}} when V == true; V == ok ->
+ {200, iolist_to_binary(Text)};
+ {{_, restuple}, {ErrorAtom, Msg}} ->
+ format_error_result(ErrorAtom, 0, Msg);
{{_, {list, _}}, _V} ->
{_, L} = format_result(Result, ResultFormat),
{200, L};
@@ -470,6 +468,11 @@ format_result({Code, Text}, {Name, restuple}) ->
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, iolist_to_binary(Text)}]}};
+format_result(Code, {Name, restuple}) ->
+ {jlib:atom_to_binary(Name),
+ {[{<<"res">>, Code == true orelse Code == ok},
+ {<<"text">>, <<"">>}]}};
+
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
@@ -488,24 +491,74 @@ format_result(Tuple, {Name, {tuple, Def}}) ->
format_result(404, {_Name, _}) ->
"not_found".
+
+format_error_result(conflict, Code, Msg) ->
+ {409, Code, iolist_to_binary(Msg)};
+format_error_result(_ErrorAtom, Code, Msg) ->
+ {500, Code, iolist_to_binary(Msg)}.
+
unauthorized_response() ->
- unauthorized_response(<<"401 Unauthorized">>).
-unauthorized_response(Body) ->
- json_response(401, jiffy:encode(Body)).
+ json_error(401, 10, <<"You are not authorized to call this command.">>).
+
+invalid_token_response() ->
+ json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
+
+outofscope_response() ->
+ json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
badrequest_response() ->
badrequest_response(<<"400 Bad Request">>).
badrequest_response(Body) ->
json_response(400, jiffy:encode(Body)).
+json_format({Code, Result}) ->
+ json_response(Code, jiffy:encode(Result));
+json_format({HTMLCode, JSONErrorCode, Message}) ->
+ json_error(HTMLCode, JSONErrorCode, Message).
+
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
+%% HTTPCode, JSONCode = integers
+%% message is binary
+json_error(HTTPCode, JSONCode, Message) ->
+ {HTTPCode, ?HEADER(?CT_JSON),
+ jiffy:encode({[{<<"status">>, <<"error">>},
+ {<<"code">>, JSONCode},
+ {<<"message">>, Message}]})
+ }.
+
log(Call, Args, {Addr, Port}) ->
AddrS = jlib:ip_to_list({Addr, Port}),
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
log(Call, Args, IP) ->
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
+permission_addon() ->
+ Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
+ fun(V) -> V end,
+ none),
+ Rules = acl:resolve_access(Access, global),
+ R = lists:filtermap(
+ fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow ->
+ {true, {[{allow, AclRules}], {[<<"*">>], []}}};
+ ({List, AclRules}) when is_list(List) ->
+ {true, {[{allow, AclRules}], {List, []}}};
+ (_) ->
+ false
+ end, Rules),
+ case R of
+ [] ->
+ none;
+ _ ->
+ {_, Res} = lists:foldl(
+ fun({R2, L2}, {Idx, Acc}) ->
+ {Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ",
+ (integer_to_binary(Idx))/binary>>,
+ {[?MODULE], [{access, R2}], L2}} | Acc]}
+ end, {1, []}, R),
+ Res
+ end.
+
mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1;
mod_opt_type(_) -> [admin_ip_access].
diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl
index fa37b801f..9522cd3d4 100644
--- a/src/mod_http_upload_quota.erl
+++ b/src/mod_http_upload_quota.erl
@@ -251,7 +251,7 @@ terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
- lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers).
+ lists:foreach(fun timer:cancel/1, Timers).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
@@ -299,7 +299,7 @@ enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) ->
{[Path | AccFiles], AccSize + Size, NewSize}
end, {[], 0, 0}, Files),
if OldSize + SlotSize > MaxSize ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles),
+ lists:foreach(fun del_file_and_dir/1, DelFiles),
file:del_dir(UserDir), % In case it's empty, now.
NewSize + SlotSize;
true ->
@@ -314,7 +314,7 @@ delete_old_files(UserDir, CutOff) ->
[] ->
ok;
OldFiles ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles),
+ lists:foreach(fun del_file_and_dir/1, OldFiles),
file:del_dir(UserDir) % In case it's empty, now.
end.
diff --git a/src/mod_irc.erl b/src/mod_irc.erl
index fefebcfa3..2fb35414d 100644
--- a/src/mod_irc.erl
+++ b/src/mod_irc.erl
@@ -85,7 +85,7 @@ start(Host, Opts) ->
start_supervisor(Host),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 4c3050df1..3d5c8f64d 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -106,15 +106,12 @@ start(Host, Opts) ->
ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE,
remove_user, 50),
case gen_mod:get_opt(assume_mam_usage, Opts,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- never ->
- ok;
- _ ->
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
- message_is_archived, 50)
+ message_is_archived, 50);
+ false ->
+ ok
end,
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@@ -159,15 +156,12 @@ stop(Host) ->
ejabberd_hooks:delete(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- never ->
- ok;
- _ ->
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
- message_is_archived, 50)
+ message_is_archived, 50);
+ false ->
+ ok
end,
ejabberd_commands:unregister_commands(get_commands_spec()),
ok.
@@ -367,32 +361,13 @@ message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) ->
true;
message_is_archived(false, C2SState, Peer,
#jid{luser = LUser, lserver = LServer}, Pkt) ->
- Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- if_enabled ->
- case get_prefs(LUser, LServer) of
- #archive_prefs{} = P ->
- {ok, P};
- error ->
- error
- end;
- on_request ->
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- cache_tab:lookup(archive_prefs, {LUser, LServer},
- fun() ->
- Mod:get_prefs(LUser, LServer)
- end);
- never ->
- error
- end,
- case Res of
- {ok, Prefs} ->
+ case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
should_archive(strip_my_archived_tag(Pkt, LServer), LServer)
- andalso should_archive_peer(C2SState, Prefs, Peer);
- error ->
+ andalso should_archive_peer(C2SState, get_prefs(LUser, LServer),
+ Peer);
+ false ->
false
end.
@@ -493,9 +468,10 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
xmpp:make_error(IQ, Err)
end.
-should_archive(#message{type = T}, _LServer) when T == error; T == result ->
+should_archive(#message{type = error}, _LServer) ->
false;
-should_archive(#message{body = Body} = Pkt, LServer) ->
+should_archive(#message{body = Body, subject = Subject,
+ type = Type} = Pkt, LServer) ->
case is_resent(Pkt, LServer) of
true ->
false;
@@ -505,14 +481,11 @@ should_archive(#message{body = Body} = Pkt, LServer) ->
true;
no_store ->
false;
+ none when Type == groupchat; Type == headline ->
+ false;
none ->
- case xmpp:get_text(Body) of
- <<>> ->
- %% Empty body
- false;
- _ ->
- true
- end
+ xmpp:get_text(Body) /= <<>> orelse
+ xmpp:get_text(Subject) /= <<>>
end
end;
should_archive(_, _LServer) ->
@@ -669,9 +642,15 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
case should_archive_peer(C2SState, Prefs, Peer) of
true ->
US = {LUser, LServer},
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- El = xmpp:encode(Pkt),
- Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir);
+ case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
+ [LUser, LServer, Peer, chat, Dir]) of
+ drop ->
+ pass;
+ NewPkt ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ El = xmpp:encode(NewPkt),
+ Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir)
+ end;
false ->
pass
end.
@@ -679,11 +658,17 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
store_muc(MUCState, Pkt, RoomJID, Peer, Nick) ->
case should_archive_muc(Pkt) of
true ->
- LServer = MUCState#state.server_host,
{U, S, _} = jid:tolower(RoomJID),
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- El = xmpp:encode(Pkt),
- Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv);
+ LServer = MUCState#state.server_host,
+ case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
+ [U, S, Peer, groupchat, recv]) of
+ drop ->
+ pass;
+ NewPkt ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ El = xmpp:encode(NewPkt),
+ Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv)
+ end;
false ->
pass
end.
@@ -879,6 +864,7 @@ is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) ->
send(Msgs, Count, IsComplete,
#iq{from = From, to = To,
sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) ->
+ Hint = #hint{type = 'no-store'},
Els = lists:map(
fun({ID, _IDInt, El}) ->
#message{sub_els = [#mam_result{xmlns = NS,
@@ -905,7 +891,7 @@ send(Msgs, Count, IsComplete,
fun(El) ->
ejabberd_router:route(To, From, El)
end, Els),
- ejabberd_router:route(To, From, #message{sub_els = [Result]}),
+ ejabberd_router:route(To, From, #message{sub_els = [Result, Hint]}),
ignore
end.
@@ -926,6 +912,8 @@ filter_by_max(_Msgs, _Junk) ->
-spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined.
limit_max(RSM, ?NS_MAM_TMP) ->
RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
+limit_max(undefined, _NS) ->
+ #rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) ->
RSM#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
@@ -972,10 +960,7 @@ get_commands_spec() ->
result = {res, rescode}}].
mod_opt_type(assume_mam_usage) ->
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end;
+ fun (B) when is_boolean(B) -> B end;
mod_opt_type(cache_life_time) ->
fun (I) when is_integer(I), I > 0 -> I end;
mod_opt_type(cache_size) ->
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
index 7ca09f4db..f7bd0ec9a 100644
--- a/src/mod_mix.erl
+++ b/src/mod_mix.erl
@@ -43,7 +43,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 5000, worker, [?MODULE]},
+ transient, 5000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 9c17643b8..ea8bff5e3 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -71,13 +71,12 @@
server_host = <<"">> :: binary(),
access = {none, none, none, none} :: {atom(), atom(), atom(), atom()},
history_size = 20 :: non_neg_integer(),
+ max_rooms_discoitems = 100 :: non_neg_integer(),
default_room_opts = [] :: list(),
room_shaper = none :: shaper:shaper()}).
-define(PROCNAME, ejabberd_mod_muc).
--define(MAX_ROOMS_DISCOITEMS, 100).
-
-type muc_room_opts() :: [{atom(), any()}].
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass.
@@ -100,7 +99,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
@@ -196,6 +195,9 @@ init([Host, Opts]) ->
HistorySize = gen_mod:get_opt(history_size, Opts,
fun(I) when is_integer(I), I>=0 -> I end,
20),
+ MaxRoomsDiscoItems = gen_mod:get_opt(max_rooms_discoitems, Opts,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 100),
DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts,
fun(L) when is_list(L) -> L end,
[]),
@@ -221,6 +223,7 @@ init([Host, Opts]) ->
public -> Bool;
public_list -> Bool;
mam -> Bool;
+ allow_subscription -> Bool;
password -> fun iolist_to_binary/1;
title -> fun iolist_to_binary/1;
allow_private_messages_from_visitors ->
@@ -272,6 +275,7 @@ init([Host, Opts]) ->
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper}}.
handle_call(stop, _From, State) ->
@@ -300,9 +304,10 @@ handle_info({route, From, To, Packet},
#state{host = Host, server_host = ServerHost,
access = Access, default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper} = State) ->
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts) of
+ From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
_ ->
@@ -339,12 +344,12 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%--------------------------------------------------------------------
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts) ->
+ From, To, Packet, DefRoomOpts, _MaxRoomsDiscoItems) ->
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
case acl:match_rule(ServerHost, AccessRoute, From) of
allow ->
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts);
+ From, To, Packet, DefRoomOpts);
deny ->
Lang = xmpp:get_lang(Packet),
ErrText = <<"Access denied by service policy">>,
@@ -487,9 +492,13 @@ process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) ->
Host = To#jid.lserver,
- xmpp:make_iq_result(
- IQ, #disco_items{node = Node,
- items = iq_disco_items(Host, From, Lang, Node, RSM)});
+ ServerHost = ejabberd_router:host_of_route(Host),
+ MaxRoomsDiscoItems = gen_mod:get_module_opt(
+ ServerHost, ?MODULE, max_rooms_discoitems,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 100),
+ Items = iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, Node, RSM),
+ xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items});
process_disco_items(#iq{lang = Lang} = IQ) ->
Txt = <<"No module is handling this query">>,
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
@@ -588,23 +597,23 @@ register_room(Host, Room, Pid) ->
end,
mnesia:transaction(F).
-iq_disco_items(Host, From, Lang, <<"">>, undefined) ->
+iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"">>, undefined) ->
Rooms = get_vh_rooms(Host),
- case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of
+ case erlang:length(Rooms) < MaxRoomsDiscoItems of
true ->
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang});
false ->
- iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined)
+ iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined)
end;
-iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined) ->
Empty = #disco_item{jid = jid:make(<<"conference.localhost">>),
node = <<"emptyrooms">>,
name = translate:translate(Lang, <<"Empty Rooms">>)},
Query = {get_disco_item, only_non_empty, From, Lang},
[Empty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
-iq_disco_items(Host, From, Lang, <<"emptyrooms">>, undefined) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"emptyrooms">>, undefined) ->
iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang});
-iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, _DiscoNode, Rsm) ->
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
RsmOut = jlib:rsm_encode(RsmO),
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut.
@@ -624,47 +633,47 @@ iq_disco_items_list(Host, Rooms, Query) ->
get_vh_rooms(_, _) ->
todo.
-%% get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
-%% AllRooms = lists:sort(get_vh_rooms(Host)),
-%% Count = erlang:length(AllRooms),
-%% Guard = case Direction of
-%% _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
-%% aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
-%% before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
-%% _ -> [{'==', {element, 2, '$1'}, Host}]
-%% end,
-%% L = lists:sort(
-%% mnesia:dirty_select(muc_online_room,
-%% [{#muc_online_room{name_host = '$1', _ = '_'},
-%% Guard,
-%% ['$_']}])),
-%% L2 = if
-%% Index == undefined andalso Direction == before ->
-%% lists:reverse(lists:sublist(lists:reverse(L), 1, M));
-%% Index == undefined ->
-%% lists:sublist(L, 1, M);
-%% Index > Count orelse Index < 0 ->
-%% [];
-%% true ->
-%% lists:sublist(L, Index+1, M)
-%% end,
-%% if L2 == [] -> {L2, #rsm_out{count = Count}};
-%% true ->
-%% H = hd(L2),
-%% NewIndex = get_room_pos(H, AllRooms),
-%% T = lists:last(L2),
-%% {F, _} = H#muc_online_room.name_host,
-%% {Last, _} = T#muc_online_room.name_host,
-%% {L2,
-%% #rsm_out{first = F, last = Last, count = Count,
-%% index = NewIndex}}
-%% end.
+ %% AllRooms = lists:sort(get_vh_rooms(Host)),
+ %% Count = erlang:length(AllRooms),
+ %% Guard = case Direction of
+ %% _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
+ %% aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
+ %% before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
+ %% _ -> [{'==', {element, 2, '$1'}, Host}]
+ %% end,
+ %% L = lists:sort(
+ %% mnesia:dirty_select(muc_online_room,
+ %% [{#muc_online_room{name_host = '$1', _ = '_'},
+ %% Guard,
+ %% ['$_']}])),
+ %% L2 = if
+ %% Index == undefined andalso Direction == before ->
+ %% lists:reverse(lists:sublist(lists:reverse(L), 1, M));
+ %% Index == undefined ->
+ %% lists:sublist(L, 1, M);
+ %% Index > Count orelse Index < 0 ->
+ %% [];
+ %% true ->
+ %% lists:sublist(L, Index+1, M)
+ %% end,
+ %% if L2 == [] -> {L2, #rsm_out{count = Count}};
+ %% true ->
+ %% H = hd(L2),
+ %% NewIndex = get_room_pos(H, AllRooms),
+ %% T = lists:last(L2),
+ %% {F, _} = H#muc_online_room.name_host,
+ %% {Last, _} = T#muc_online_room.name_host,
+ %% {L2,
+ %% #rsm_out{first = F, last = Last, count = Count,
+ %% index = NewIndex}}
+ %% end.
get_subscribed_rooms(_ServerHost, Host, From) ->
Rooms = get_vh_rooms(Host),
+ BareFrom = jid:remove_resource(From),
lists:flatmap(
fun(#muc_online_room{name_host = {Name, _}, pid = Pid}) ->
- case gen_fsm:sync_send_all_state_event(Pid, {is_subscriber, From}) of
+ case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of
true -> [jid:make(Name, Host)];
false -> []
end;
@@ -874,6 +883,8 @@ mod_opt_type(max_room_id) ->
fun (infinity) -> infinity;
(I) when is_integer(I), I > 0 -> I
end;
+mod_opt_type(max_rooms_discoitems) ->
+ fun (I) when is_integer(I), I >= 0 -> I end;
mod_opt_type(regexp_room_id) ->
fun iolist_to_binary/1;
mod_opt_type(max_room_name) ->
@@ -901,8 +912,8 @@ mod_opt_type(user_presence_shaper) ->
mod_opt_type(_) ->
[access, access_admin, access_create, access_persistent,
db_type, default_room_options, history_size, host,
- max_room_desc, max_room_id, max_room_name, regexp_room_id,
- max_user_conferences, max_users,
+ max_room_desc, max_room_id, max_room_name,
+ max_rooms_discoitems, max_user_conferences, max_users,
max_users_admin_threshold, max_users_presence,
min_message_interval, min_presence_interval,
- room_shaper, user_message_shaper, user_presence_shaper].
+ regexp_room_id, room_shaper, user_message_shaper, user_presence_shaper].
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 4d56093e1..8f1f649d2 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -13,6 +13,7 @@
-export([start/2, stop/1, depends/2, muc_online_rooms/1,
muc_unregister_nick/1, create_room/3, destroy_room/2,
+ create_room_with_opts/4,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
get_user_rooms/2, get_room_occupants/2,
@@ -20,6 +21,7 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2,
web_menu_main/2, web_page_main/2, web_menu_host/3,
+ subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
web_page_host/3, mod_opt_type/1, get_commands_spec/0]).
-include("ejabberd.hrl").
@@ -87,6 +89,18 @@ get_commands_spec() ->
module = ?MODULE, function = create_rooms_file,
args = [{file, string}],
result = {res, rescode}},
+ #ejabberd_commands{name = create_room_with_opts, tags = [muc_room],
+ desc = "Create a MUC room name@service in host with given options",
+ module = ?MODULE, function = create_room_with_opts,
+ args = [{name, binary}, {service, binary},
+ {host, binary},
+ {options, {list,
+ {option, {tuple,
+ [{name, binary},
+ {value, binary}
+ ]}}
+ }}],
+ result = {res, rescode}},
#ejabberd_commands{name = destroy_rooms_file, tags = [muc],
desc = "Destroy the rooms indicated in file",
longdesc = "Provide one room JID per line.",
@@ -151,7 +165,22 @@ get_commands_spec() ->
{value, string}
]}}
}}},
-
+ #ejabberd_commands{name = subscribe_room, tags = [muc_room],
+ desc = "Subscribe to a MUC conference",
+ module = ?MODULE, function = subscribe_room,
+ args = [{user, binary}, {nick, binary}, {room, binary},
+ {nodes, binary}],
+ result = {nodes, {list, {node, string}}}},
+ #ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
+ desc = "Unsubscribe from a MUC conference",
+ module = ?MODULE, function = unsubscribe_room,
+ args = [{user, binary}, {room, binary}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = get_subscribers, tags = [muc_room],
+ desc = "List subscribers of a MUC conference",
+ module = ?MODULE, function = get_subscribers,
+ args = [{name, binary}, {service, binary}],
+ result = {subscribers, {list, {jid, string}}}},
#ejabberd_commands{name = set_room_affiliation, tags = [muc_room],
desc = "Change an affiliation in a MUC room",
module = ?MODULE, function = set_room_affiliation,
@@ -400,15 +429,23 @@ prepare_room_info(Room_info) ->
%% ok | error
%% @doc Create a room immediately with the default options.
create_room(Name1, Host1, ServerHost) ->
- Name = jid:nodeprep(Name1),
- Host = jid:nodeprep(Host1),
+ create_room_with_opts(Name1, Host1, ServerHost, []).
+
+create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
+ true = (error /= (Name = jid:nodeprep(Name1))),
+ true = (error /= (Host = jid:nodeprep(Host1))),
%% Get the default room options from the muc configuration
DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
default_room_options, fun(X) -> X end, []),
+ %% Change default room options as required
+ FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts],
+ RoomOpts = lists:ukeymerge(1,
+ lists:keysort(1, FormattedRoomOpts),
+ lists:keysort(1, DefRoomOpts)),
%% Store the room on the server, it is not started yet though at this point
- mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts),
+ mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
%% Get all remaining mod_muc parameters that might be utilized
Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all),
@@ -429,7 +466,7 @@ create_room(Name1, Host1, ServerHost) ->
Name,
HistorySize,
RoomShaper,
- DefRoomOpts),
+ RoomOpts),
{atomic, ok} = register_room(Host, Name, Pid),
ok;
_ ->
@@ -477,7 +514,7 @@ destroy_room({N, H, SH}) ->
%% The file encoding must be UTF-8
destroy_rooms_file(Filename) ->
- {ok, F} = file:open(Filename, [read, binary]),
+ {ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@@ -496,7 +533,7 @@ read_room(F) ->
eof -> eof;
String ->
case io_lib:fread("~s", String) of
- {ok, [RoomJID], _} -> split_roomjid(RoomJID);
+ {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID));
{error, What} ->
io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String])
end
@@ -514,7 +551,7 @@ split_roomjid(RoomJID) ->
%%----------------------------
create_rooms_file(Filename) ->
- {ok, F} = file:open(Filename, [read, binary]),
+ {ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@@ -748,12 +785,20 @@ send_direct_invitation(FromJid, UserJid, XmlEl) ->
%% the option to change (for example title or max_users),
%% and the value to assign to the new option.
%% For example:
-%% change_room_option("testroom", "conference.localhost", "title", "Test Room")
-change_room_option(Name, Service, Option, Value) when is_atom(Option) ->
- Pid = get_room_pid(Name, Service),
- {ok, _} = change_room_option(Pid, Option, Value),
- ok;
+%% change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)
change_room_option(Name, Service, OptionString, ValueString) ->
+ case get_room_pid(Name, Service) of
+ room_not_found ->
+ room_not_found;
+ Pid ->
+ {Option, Value} = format_room_option(OptionString, ValueString),
+ Config = get_room_config(Pid),
+ Config2 = change_option(Option, Value, Config),
+ {ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}),
+ ok
+ end.
+
+format_room_option(OptionString, ValueString) ->
Option = jlib:binary_to_atom(OptionString),
Value = case Option of
title -> ValueString;
@@ -764,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) ->
max_users -> binary_to_integer(ValueString);
_ -> jlib:binary_to_atom(ValueString)
end,
- change_room_option(Name, Service, Option, Value).
-
-change_room_option(Pid, Option, Value) ->
- Config = get_room_config(Pid),
- Config2 = change_option(Option, Value, Config),
- gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}).
+ {Option, Value}.
%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
get_room_pid(Name, Service) ->
@@ -789,6 +829,7 @@ change_option(Option, Value, Config) ->
allow_private_messages -> Config#config{allow_private_messages = Value};
allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value};
allow_query_users -> Config#config{allow_query_users = Value};
+ allow_subscription -> Config#config{allow_subscription = Value};
allow_user_invites -> Config#config{allow_user_invites = Value};
allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value};
allow_visitor_status -> Config#config{allow_visitor_status = Value};
@@ -884,6 +925,74 @@ set_room_affiliation(Name, Service, JID, AffiliationString) ->
error
end.
+%%%
+%%% MUC Subscription
+%%%
+
+subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> ->
+ throw({error, "Nickname must be set"});
+subscribe_room(User, Nick, Room, Nodes) ->
+ NodeList = re:split(Nodes, "\\h*,\\h*"),
+ case jid:from_string(Room) of
+ #jid{luser = Name, lserver = Host} when Name /= <<"">> ->
+ case jid:from_string(User) of
+ error ->
+ throw({error, "Malformed user JID"});
+ #jid{lresource = <<"">>} ->
+ throw({error, "User's JID should have a resource"});
+ UserJID ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ case gen_fsm:sync_send_all_state_event(
+ Pid,
+ {muc_subscribe, UserJID, Nick, NodeList}) of
+ {ok, SubscribedNodes} ->
+ SubscribedNodes;
+ {error, Reason} ->
+ throw({error, binary_to_list(Reason)})
+ end;
+ _ ->
+ throw({error, "The room does not exist"})
+ end
+ end;
+ _ ->
+ throw({error, "Malformed room JID"})
+ end.
+
+unsubscribe_room(User, Room) ->
+ case jid:from_string(Room) of
+ #jid{luser = Name, lserver = Host} when Name /= <<"">> ->
+ case jid:from_string(User) of
+ error ->
+ throw({error, "Malformed user JID"});
+ UserJID ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ case gen_fsm:sync_send_all_state_event(
+ Pid,
+ {muc_unsubscribe, UserJID}) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ throw({error, binary_to_list(Reason)})
+ end;
+ _ ->
+ throw({error, "The room does not exist"})
+ end
+ end;
+ _ ->
+ throw({error, "Malformed room JID"})
+ end.
+
+get_subscribers(Name, Host) ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ {ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers),
+ [jid:to_string(jid:remove_resource(J)) || J <- JIDList];
+ _ ->
+ throw({error, "The room does not exist"})
+ end.
+
make_opts(StateData) ->
Config = StateData#state.config,
[
diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl
index 4b129ce81..5cf52e60f 100644
--- a/src/mod_muc_log.erl
+++ b/src/mod_muc_log.erl
@@ -81,7 +81,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index ce6851bc5..c83565734 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -142,6 +142,7 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
normal_state({route, From, <<"">>,
#message{type = Type, lang = Lang} = Packet}, StateData) ->
case is_user_online(From, StateData) orelse
+ is_subscriber(From, StateData) orelse
is_user_allowed_message_nonparticipant(From, StateData) of
true when Type == groupchat ->
Activity = get_user_activity(From, StateData),
@@ -372,7 +373,8 @@ normal_state({route, From, ToNick,
{next_state, normal_state, StateData};
continue_delivery ->
case {(StateData#state.config)#config.allow_private_messages,
- is_user_online(From, StateData)} of
+ is_user_online(From, StateData) orelse
+ is_subscriber(From, StateData)} of
{true, true} when Type == groupchat ->
ErrText = <<"It is not allowed to send private messages "
"of type \"groupchat\"">>,
@@ -397,9 +399,7 @@ normal_state({route, From, ToNick,
PmFromVisitors == anyone;
(PmFromVisitors == moderators) and
DstIsModerator ->
- {ok, #user{nick = FromNick}} =
- (?DICT):find(jid:tolower(From),
- StateData#state.users),
+ {FromNick, _} = get_participant_data(From, StateData),
FromNickJID =
jid:replace_resource(StateData#state.jid,
FromNick),
@@ -476,7 +476,7 @@ handle_event({service_message, Msg}, _StateName,
MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
send_wrapped_multiple(
StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@@ -538,8 +538,59 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
NSD ->
{reply, {ok, NSD}, StateName, NSD}
end;
-handle_sync_event({is_subscriber, From}, _From, StateName, StateData) ->
- {reply, is_subscriber(From, StateData), StateName, StateData};
+handle_sync_event(get_subscribers, _From, StateName, StateData) ->
+ JIDs = lists:map(fun jid:make/1,
+ ?DICT:fetch_keys(StateData#state.subscribers)),
+ {reply, {ok, JIDs}, StateName, StateData};
+handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
+ StateName, StateData) ->
+ IQ = #iq{type = set, id = randoms:get_string(),
+ from = From, sub_els = [#muc_subscribe{nick = Nick,
+ events = Nodes}]},
+ Config = StateData#state.config,
+ CaptchaRequired = Config#config.captcha_protected,
+ PasswordProtected = Config#config.password_protected,
+ TmpConfig = Config#config{captcha_protected = false,
+ password_protected = false},
+ TmpState = StateData#state{config = TmpConfig},
+ case process_iq_mucsub(From, IQ, TmpState) of
+ {result, #muc_subscribe{events = NewNodes}, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {ok, NewNodes}, StateName,
+ NewState#state{config = NewConfig}};
+ {ignore, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {error, <<"Requrest is ignored">>},
+ NewState#state{config = NewConfig}};
+ {error, Err, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {error, get_error_text(Err)}, StateName,
+ NewState#state{config = NewConfig}};
+ {error, Err} ->
+ {reply, {error, get_error_text(Err)}, StateName, StateData}
+ end;
+handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) ->
+ IQ = #iq{type = set, id = randoms:get_string(),
+ from = From, sub_els = [#muc_unsubscribe{}]},
+ case process_iq_mucsub(From, IQ, StateData) of
+ {result, _, NewState} ->
+ {reply, ok, StateName, NewState};
+ {ignore, NewState} ->
+ {reply, {error, <<"Requrest is ignored">>}, NewState};
+ {error, Err, NewState} ->
+ {reply, {error, get_error_text(Err)}, StateName, NewState};
+ {error, Err} ->
+ {reply, {error, get_error_text(Err)}, StateName, StateData}
+ end;
+handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
+ IsSubs = ?DICT:is_key(jid:split(From), StateData#state.subscribers),
+ {reply, IsSubs, StateName, StateData};
handle_sync_event(_Event, _From, StateName,
StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
@@ -654,7 +705,7 @@ terminate(Reason, _StateName, StateData) ->
end,
tab_remove_online_user(LJID, StateData)
end,
- [], StateData#state.users),
+ [], get_users_and_subscribers(StateData)),
add_to_log(room_existence, stopped, StateData),
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
@@ -669,11 +720,12 @@ route(Pid, From, ToNick, Packet) ->
-spec process_groupchat_message(jid(), message(), state()) -> fsm_next().
process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
- case is_user_online(From, StateData) orelse
+ IsSubscriber = is_subscriber(From, StateData),
+ case is_user_online(From, StateData) orelse IsSubscriber orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
- {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData),
+ {FromNick, Role} = get_participant_data(From, StateData),
if (Role == moderator) or (Role == participant) or IsSubscriber or
((StateData#state.config)#config.moderated == false) ->
Subject = check_subject(Packet),
@@ -682,6 +734,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
_ ->
case
can_change_subject(Role,
+ IsSubscriber,
StateData)
of
true ->
@@ -716,7 +769,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
- StateData#state.users,
+ get_users_and_subscribers(StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@@ -907,14 +960,21 @@ is_user_allowed_message_nonparticipant(JID,
%% @doc Get information of this participant, or default values.
%% If the JID is not a participant, return values for a service message.
--spec get_participant_data(jid(), state()) -> {binary(), role(), boolean()}.
+-spec get_participant_data(jid(), state()) -> {binary(), role()}.
get_participant_data(From, StateData) ->
case (?DICT):find(jid:tolower(From),
StateData#state.users)
of
- {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} ->
- {FromNick, Role, IsSubscriber};
- error -> {<<"">>, moderator, false}
+ {ok, #user{nick = FromNick, role = Role}} ->
+ {FromNick, Role};
+ error ->
+ case ?DICT:find(jid:tolower(jid:remove_resource(From)),
+ StateData#state.subscribers) of
+ {ok, #subscriber{nick = FromNick}} ->
+ {FromNick, none};
+ error ->
+ {<<"">>, moderator}
+ end
end.
-spec process_presence(jid(), binary(), presence(), state()) -> fsm_transition().
@@ -979,32 +1039,19 @@ do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packe
From, Packet, Err),
StateData;
_ ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
change_nick(From, Nick, StateData)
- end
end;
- _NotNickChange ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
- Stanza = maybe_strip_status_from_presence(
- From, Packet, StateData),
- NewState = add_user_presence(From, Stanza,
- StateData),
- send_new_presence(From, NewState, StateData),
- NewState
- end
+ false ->
+ Stanza = maybe_strip_status_from_presence(
+ From, Packet, StateData),
+ NewState = add_user_presence(From, Stanza,
+ StateData),
+ send_new_presence(From, NewState, StateData),
+ NewState
end
end;
do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
StateData) ->
- IsSubscriber = is_subscriber(From, StateData),
NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
is_visitor(From, StateData)} of
{false, true} ->
@@ -1017,7 +1064,7 @@ do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
_ -> send_new_presence(From, NewState, StateData)
end,
Reason = xmpp:get_text(NewPacket#presence.status),
- remove_online_user(From, NewState, IsSubscriber, Reason);
+ remove_online_user(From, NewState, Reason);
do_process_presence(From, _Nick, #presence{type = error, lang = Lang} = Packet,
StateData) ->
ErrorText = <<"It is not allowed to send error messages to the"
@@ -1036,24 +1083,11 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
_Allowed -> Packet
end.
--spec subscriber_becomes_available(jid(), binary(), presence(),
- state()) -> state().
-subscriber_becomes_available(From, Nick, Packet, StateData) ->
- Stanza = maybe_strip_status_from_presence(From, Packet, StateData),
- State1 = add_user_presence(From, Stanza, StateData),
- Aff = get_affiliation(From, State1),
- Role = get_default_role(Aff, State1),
- State2 = set_role(From, Role, State1),
- State3 = set_nick(From, Nick, State2),
- send_existing_presences(From, State3),
- send_initial_presence(From, State3, StateData),
- State3.
-
-spec close_room_if_temporary_and_empty(state()) -> fsm_transition().
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
- andalso (?DICT):size(StateData1#state.users) == 0
- of
+ andalso (?DICT):size(StateData1#state.users) == 0
+ andalso (?DICT):size(StateData1#state.subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
"and empty",
@@ -1063,6 +1097,32 @@ close_room_if_temporary_and_empty(StateData1) ->
_ -> {next_state, normal_state, StateData1}
end.
+get_users_and_subscribers(StateData) ->
+ OnlineSubscribers = ?DICT:fold(
+ fun(LJID, _, Acc) ->
+ LBareJID = jid:remove_resource(LJID),
+ case is_subscriber(LBareJID, StateData) of
+ true ->
+ ?SETS:add_element(LBareJID, Acc);
+ false ->
+ Acc
+ end
+ end, ?SETS:new(), StateData#state.users),
+ ?DICT:fold(
+ fun(LBareJID, #subscriber{nick = Nick}, Acc) ->
+ case ?SETS:is_element(LBareJID, OnlineSubscribers) of
+ false ->
+ ?DICT:store(LBareJID,
+ #user{jid = jid:make(LBareJID),
+ nick = Nick,
+ role = none,
+ last_presence = undefined},
+ Acc);
+ true ->
+ Acc
+ end
+ end, StateData#state.users, StateData#state.subscribers).
+
-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
LJID = jid:tolower(JID),
@@ -1070,13 +1130,8 @@ is_user_online(JID, StateData) ->
-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
- LJID = jid:tolower(JID),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = IsSubscriber}} ->
- IsSubscriber;
- _ ->
- false
- end.
+ LJID = jid:tolower(jid:remove_resource(JID)),
+ (?DICT):is_key(LJID, StateData#state.subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
@@ -1201,6 +1256,14 @@ get_error_condition(#stanza_error{reason = Reason}) ->
get_error_condition(undefined) ->
"undefined".
+get_error_text(Error) ->
+ case fxml:get_subtag_with_xmlns(Error, <<"text">>, ?NS_STANZAS) of
+ #xmlel{} = Tag ->
+ fxml:get_tag_cdata(Tag);
+ false ->
+ <<"">>
+ end.
+
-spec make_reason(stanza(), jid(), state(), binary()) -> binary().
make_reason(Packet, From, StateData, Reason1) ->
{ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users),
@@ -1210,14 +1273,13 @@ make_reason(Packet, From, StateData, Reason1) ->
-spec expulse_participant(stanza(), jid(), state(), binary()) ->
state().
expulse_participant(Packet, From, StateData, Reason1) ->
- IsSubscriber = is_subscriber(From, StateData),
Reason2 = make_reason(Packet, From, StateData, Reason1),
NewState = add_user_presence_un(From,
#presence{type = unavailable,
status = xmpp:mk_text(Reason2)},
StateData),
send_new_presence(From, NewState, StateData),
- remove_online_user(From, NewState, IsSubscriber).
+ remove_online_user(From, NewState).
-spec set_affiliation(jid(), affiliation(), state()) -> state().
set_affiliation(JID, Affiliation, StateData) ->
@@ -1514,8 +1576,7 @@ prepare_room_queue(StateData) ->
end.
-spec update_online_user(jid(), #user{}, state()) -> state().
-update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
- is_subscriber = IsSubscriber} = User, StateData) ->
+update_online_user(JID, #user{nick = Nick} = User, StateData) ->
LJID = jid:tolower(JID),
Nicks1 = case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{nick = OldNick}} ->
@@ -1534,9 +1595,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
[LJID], Nicks1),
Users = (?DICT):update(LJID,
fun(U) ->
- U#user{nick = Nick,
- subscriptions = Nodes,
- is_subscriber = IsSubscriber}
+ U#user{nick = Nick}
end, User, StateData#state.users),
NewStateData = StateData#state{users = Users, nicks = Nicks},
case {?DICT:find(LJID, StateData#state.users),
@@ -1548,35 +1607,32 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
end,
NewStateData.
--spec add_online_user(jid(), binary(), role(), boolean(), [binary()], state()) -> state().
-add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) ->
+set_subscriber(JID, Nick, Nodes, StateData) ->
+ BareJID = jid:remove_resource(JID),
+ LBareJID = jid:tolower(BareJID),
+ Subscribers = ?DICT:store(LBareJID,
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ StateData#state.subscribers),
+ Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
+ store_room(NewStateData),
+ NewStateData.
+
+-spec add_online_user(jid(), binary(), role(), state()) -> state().
+add_online_user(JID, Nick, Role, StateData) ->
tab_add_online_user(JID, StateData),
- User = #user{jid = JID, nick = Nick, role = Role,
- is_subscriber = IsSubscriber, subscriptions = Nodes},
- StateData1 = update_online_user(JID, User, StateData),
- if IsSubscriber ->
- store_room(StateData1);
- true ->
- ok
- end,
- StateData1.
+ User = #user{jid = JID, nick = Nick, role = Role},
+ update_online_user(JID, User, StateData).
--spec remove_online_user(jid(), state(), boolean()) -> state().
-remove_online_user(JID, StateData, IsSubscriber) ->
- remove_online_user(JID, StateData, IsSubscriber, <<"">>).
+-spec remove_online_user(jid(), state()) -> state().
+remove_online_user(JID, StateData) ->
+ remove_online_user(JID, StateData, <<"">>).
--spec remove_online_user(jid(), state(), boolean(), binary()) -> state().
-remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) ->
- LJID = jid:tolower(JID),
- Users = case (?DICT):find(LJID, StateData#state.users) of
- {ok, U} ->
- (?DICT):store(LJID, U#user{last_presence = undefined},
- StateData#state.users);
- error ->
- StateData#state.users
- end,
- StateData#state{users = Users};
-remove_online_user(JID, StateData, _IsSubscriber, Reason) ->
+-spec remove_online_user(jid(), state(), binary()) -> state().
+remove_online_user(JID, StateData, Reason) ->
LJID = jid:tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
@@ -1636,7 +1692,10 @@ add_user_presence_un(JID, Presence, StateData) ->
%% Return jid record.
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
- case (?DICT):find(Nick, StateData#state.nicks) of
+ Nicks = ?DICT:merge(fun(_, Val, _) -> Val end,
+ StateData#state.nicks,
+ StateData#state.subscriber_nicks),
+ case (?DICT):find(Nick, Nicks) of
{ok, [User]} -> [jid:make(User)];
{ok, Users} -> [jid:make(LJID) || LJID <- Users];
error -> []
@@ -1704,7 +1763,14 @@ is_nick_change(JID, Nick, StateData) ->
-spec nick_collision(jid(), binary(), state()) -> boolean().
nick_collision(User, Nick, StateData) ->
- UserOfNick = find_jid_by_nick(Nick, StateData),
+ UserOfNick = case find_jid_by_nick(Nick, StateData) of
+ false ->
+ case ?DICT:find(Nick, StateData#state.subscriber_nicks) of
+ {ok, [J]} -> J;
+ error -> false
+ end;
+ J -> J
+ end,
(UserOfNick /= false andalso
jid:remove_resource(jid:tolower(UserOfNick))
/= jid:remove_resource(jid:tolower(User))).
@@ -1819,8 +1885,7 @@ add_new_user(From, Nick, Packet, StateData) ->
NewState = add_user_presence(
From, Packet,
add_online_user(From, Nick, Role,
- IsSubscribeRequest,
- Nodes, StateData)),
+ StateData)),
send_existing_presences(From, NewState),
send_initial_presence(From, NewState, StateData),
History = get_history(Nick, Packet, NewState),
@@ -1828,9 +1893,7 @@ add_new_user(From, Nick, Packet, StateData) ->
send_subject(From, StateData),
NewState;
true ->
- add_online_user(From, Nick, none,
- IsSubscribeRequest,
- Nodes, StateData)
+ set_subscriber(From, Nick, Nodes, StateData)
end,
ResultState =
case NewStateData#state.just_created of
@@ -2020,16 +2083,6 @@ presence_broadcast_allowed(JID, StateData) ->
Role = get_role(JID, StateData),
lists:member(Role, (StateData#state.config)#config.presence_broadcast).
--spec is_initial_presence(jid(), state()) -> boolean().
-is_initial_presence(From, StateData) ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{last_presence = Pres}} when Pres /= undefined ->
- false;
- _ ->
- true
- end.
-
-spec send_initial_presence(jid(), state(), state()) -> ok.
send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@@ -2126,7 +2179,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
[{LNJID, UserInfo}];
false ->
- (?DICT):to_list(StateData#state.users)
+ (?DICT):to_list(get_users_and_subscribers(StateData))
end,
lists:foreach(
fun({LUJID, Info}) ->
@@ -2158,7 +2211,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) and
(IsInitialPresence or (Type == unavailable)) ->
@@ -2306,20 +2359,21 @@ send_nick_changing(JID, OldNick, StateData,
(_) ->
ok
end,
- (?DICT):to_list(StateData#state.users)).
+ ?DICT:to_list(get_users_and_subscribers(StateData))).
-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
+ Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
not (?DICT):is_empty(
(?DICT):filter(fun({U, S, _}, _) ->
U == LUser andalso
S == LServer
- end, StateData#state.users));
+ end, Users));
{_LUser, _LServer, _LResource} ->
- (?DICT):is_key(LJID, StateData#state.users)
+ (?DICT):is_key(LJID, Users)
end,
case IsOccupant of
true ->
@@ -2335,19 +2389,19 @@ send_affiliation(JID, Affiliation, StateData) ->
role = none},
Message = #message{id = randoms:get_string(),
sub_els = [#muc_user{items = [Item]}]},
+ Users = get_users_and_subscribers(StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
(?DICT):filter(fun(_, #user{role = moderator}) ->
true;
(_, _) ->
false
- end, StateData#state.users);
+ end, Users);
false ->
- StateData#state.users
+ Users
end,
- send_multiple(StateData#state.jid,
- StateData#state.server_host,
- Recipients, Message).
+ send_wrapped_multiple(StateData#state.jid, Recipients, Message,
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData).
-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
@@ -2452,11 +2506,11 @@ check_subject(#message{subject = [_|_] = Subj, body = [],
check_subject(_) ->
false.
--spec can_change_subject(role(), state()) -> boolean().
-can_change_subject(Role, StateData) ->
+-spec can_change_subject(role(), boolean(), state()) -> boolean().
+can_change_subject(Role, IsSubscriber, StateData) ->
case (StateData#state.config)#config.allow_change_subj
of
- true -> Role == moderator orelse Role == participant;
+ true -> Role == moderator orelse Role == participant orelse IsSubscriber == true;
_ -> Role == moderator
end.
@@ -2925,7 +2979,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) ->
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
@@ -2934,7 +2988,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
ok
end
end,
- (?DICT):to_list(StateData#state.users)).
+ (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec get_actor_nick(binary() | jid(), state()) -> binary().
get_actor_nick(<<"">>, _StateData) ->
@@ -3301,7 +3355,7 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
id = randoms:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData);
@@ -3321,7 +3375,7 @@ remove_nonmembers(StateData) ->
_ -> SD
end
end,
- StateData, (?DICT):to_list(StateData#state.users)).
+ StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec set_opts([{atom(), any()}], state()) -> state().
set_opts([], StateData) -> StateData;
@@ -3443,14 +3497,17 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
subscribers ->
- lists:foldl(
- fun({JID, Nick, Nodes}, State) ->
- User = #user{jid = JID, nick = Nick,
- subscriptions = Nodes,
- is_subscriber = true,
- role = none},
- update_online_user(JID, User, State)
- end, StateData, Val);
+ Subscribers = lists:foldl(
+ fun({JID, Nick, Nodes}, Acc) ->
+ BareJID = jid:remove_resource(JID),
+ ?DICT:store(
+ jid:tolower(BareJID),
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ Acc)
+ end, ?DICT:new(), Val),
+ StateData#state{subscribers = Subscribers};
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
@@ -3466,12 +3523,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
make_opts(StateData) ->
Config = StateData#state.config,
Subscribers = (?DICT):fold(
- fun(_LJID, #user{is_subscriber = true} = User, Acc) ->
- [{User#user.jid, User#user.nick,
- User#user.subscriptions}|Acc];
- (_, _, Acc) ->
- Acc
- end, [], StateData#state.users),
+ fun(_LJID, Sub, Acc) ->
+ [{Sub#subscriber.jid,
+ Sub#subscriber.nick,
+ Sub#subscriber.nodes}|Acc]
+ end, [], StateData#state.subscribers),
[?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
?MAKE_CONFIG_OPT(#config.allow_change_subj),
?MAKE_CONFIG_OPT(#config.allow_query_users),
@@ -3490,6 +3546,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous),
?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users),
?MAKE_CONFIG_OPT(#config.allow_voice_requests),
+ ?MAKE_CONFIG_OPT(#config.allow_subscription),
?MAKE_CONFIG_OPT(#config.mam),
?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
?MAKE_CONFIG_OPT(#config.vcard),
@@ -3517,7 +3574,7 @@ destroy_room(DEl, StateData) ->
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
end,
- (?DICT):to_list(StateData#state.users)),
+ (?DICT):to_list(get_users_and_subscribers(StateData))),
case (StateData#state.config)#config.persistent of
true ->
mod_muc:forget_room(StateData#state.server_host,
@@ -3655,9 +3712,9 @@ process_iq_mucsub(From,
#iq{type = set, lang = Lang,
sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
StateData) ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick ->
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case (?DICT):find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick1}} when Nick1 /= Nick ->
Nodes = get_subscription_nodes(Packet),
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
@@ -3670,54 +3727,53 @@ process_iq_mucsub(From,
ErrText = <<"That nickname is registered by another person">>,
{error, xmpp:err_conflict(ErrText, Lang)};
_ ->
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData}
end;
- {ok, #user{role = Role}} ->
+ {ok, #subscriber{}} ->
Nodes = get_subscription_nodes(Packet),
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData};
error ->
add_new_user(From, Nick, Packet, StateData)
end;
process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
StateData) ->
- LJID = jid:tolower(From),
- case ?DICT:find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = true} = User} ->
- NewStateData = remove_subscription(From, User, StateData),
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case ?DICT:find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick}} ->
+ Nicks = ?DICT:erase(Nick, StateData#state.subscriber_nicks),
+ Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
store_room(NewStateData),
{result, undefined, NewStateData};
_ ->
{result, undefined, StateData}
end;
+process_iq_mucsub(From, #iq{type = get, lang = Lang,
+ sub_els = [#muc_subscriptions{}]},
+ StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ FRole = get_role(From, StateData),
+ if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
+ JIDs = dict:fold(
+ fun(_, #subscriber{jid = J}, Acc) ->
+ [J|Acc]
+ end, [], StateData#state.subscribers),
+ {result, #muc_subscriptions{list = JIDs}, StateData};
+ true ->
+ Txt = <<"Moderator privileges required">>,
+ {error, xmpp:err_forbidden(Txt, Lang)}
+ end;
process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
{error, xmpp:err_bad_request(Txt, Lang)}.
--spec remove_subscription(jid(), #user{}, state()) -> state().
-remove_subscription(JID, #user{is_subscriber = true} = User, StateData) ->
- case User#user.last_presence of
- undefined ->
- remove_online_user(JID, StateData, false);
- _ ->
- LJID = jid:tolower(JID),
- Users = ?DICT:store(LJID, User#user{is_subscriber = false},
- StateData#state.users),
- StateData#state{users = Users}
- end;
-remove_subscription(_JID, #user{}, StateData) ->
- StateData.
-
--spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
- dict:fold(
- fun(_LJID, User, State) ->
- remove_subscription(User#user.jid, User, State)
- end, StateData, StateData#state.users);
+ StateData#state{subscribers = ?DICT:new(),
+ subscriber_nicks = ?DICT:new()};
true ->
StateData
end.
@@ -3957,18 +4013,26 @@ store_room(StateData) ->
-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok.
send_wrapped(From, To, Packet, Node, State) ->
LTo = jid:tolower(To),
- case ?DICT:find(LTo, State#state.users) of
- {ok, #user{is_subscriber = true,
- subscriptions = Nodes,
- last_presence = undefined}} ->
+ LBareTo = jid:tolower(jid:remove_resource(To)),
+ IsOffline = case ?DICT:find(LTo, State#state.users) of
+ {ok, #user{last_presence = undefined}} -> true;
+ error -> true;
+ _ -> false
+ end,
+ if IsOffline ->
+ case ?DICT:find(LBareTo, State#state.subscribers) of
+ {ok, #subscriber{nodes = Nodes, jid = JID}} ->
case lists:member(Node, Nodes) of
true ->
- NewPacket = wrap(From, To, Packet, Node),
- ejabberd_router:route(State#state.jid, To, NewPacket);
+ NewPacket = wrap(From, JID, Packet, Node),
+ ejabberd_router:route(State#state.jid, JID, NewPacket);
false ->
ok
end;
_ ->
+ ok
+ end;
+ true ->
ejabberd_router:route(From, To, Packet)
end.
@@ -3983,13 +4047,10 @@ wrap(From, To, Packet, Node) ->
id = randoms:get_string(),
xml_els = [El]}]}}]}.
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%% Multicast
-
--spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
-send_multiple(From, Server, Users, Packet) ->
- JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
- ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
+%% -spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
+%% send_multiple(From, Server, Users, Packet) ->
+%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
+%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
-spec send_wrapped_multiple(jid(), [#user{}], stanza(), binary(), state()) -> ok.
send_wrapped_multiple(From, Users, Packet, Node, State) ->
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 6134823c1..dfe3c9e8e 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -450,12 +450,12 @@ need_to_store(LServer, #message{type = Type} = Packet) ->
(unless_chat_state) -> unless_chat_state
end,
unless_chat_state) of
+ true ->
+ true;
false ->
Packet#message.body /= [];
unless_chat_state ->
- not xmpp_util:is_standalone_chat_state(Packet);
- true ->
- true
+ not xmpp_util:is_standalone_chat_state(Packet)
end
end;
true ->
@@ -469,14 +469,20 @@ store_packet(From, To, Packet) ->
case check_event(From, To, Packet) of
true ->
#jid{luser = LUser, lserver = LServer} = To,
- TimeStamp = p1_time_compat:timestamp(),
- Expire = find_x_expire(TimeStamp, Packet),
- El = xmpp:encode(Packet),
- gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) !
- #offline_msg{us = {LUser, LServer},
- timestamp = TimeStamp, expire = Expire,
- from = From, to = To, packet = El},
- stop;
+ case ejabberd_hooks:run_fold(store_offline_message, LServer,
+ Packet, [From, To]) of
+ drop ->
+ ok;
+ NewPacket ->
+ TimeStamp = p1_time_compat:timestamp(),
+ Expire = find_x_expire(TimeStamp, NewPacket),
+ El = xmpp:encode(NewPacket),
+ gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) !
+ #offline_msg{us = {LUser, LServer},
+ timestamp = TimeStamp, expire = Expire,
+ from = From, to = To, packet = El},
+ stop
+ end;
_ -> ok
end;
false -> ok
diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl
index b5033c710..9459753bc 100644
--- a/src/mod_offline_sql.erl
+++ b/src/mod_offline_sql.erl
@@ -81,7 +81,7 @@ remove_old_messages(Days, LServer) ->
[<<"DELETE FROM spool"
" WHERE created_at < "
"NOW() - INTERVAL '">>,
- integer_to_list(Days), <<"';">>]) of
+ integer_to_list(Days), <<"' DAY;">>]) of
{updated, N} ->
?INFO_MSG("~p message(s) deleted from offline spool", [N]);
_Error ->
diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl
index 7ca19b5e9..10f3cddc8 100644
--- a/src/mod_privacy_sql.erl
+++ b/src/mod_privacy_sql.erl
@@ -233,7 +233,7 @@ export(Server) ->
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
- " %(MatchPresenceOut)b)")
+ " %(MatchPresenceOut)b);")
|| {SType, SValue, SAction, Order,
MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn,
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
new file mode 100644
index 000000000..50212b7ae
--- /dev/null
+++ b/src/mod_privilege.erl
@@ -0,0 +1,348 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 11 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_privilege).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+-export([component_connected/1, component_disconnected/2,
+ roster_access/2, process_message/3,
+ process_presence_out/4, process_presence_in/5]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("xmpp.hrl").
+
+-record(state, {server_host = <<"">> :: binary(),
+ permissions = dict:new() :: ?TDICT}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(ejabberd_sup, PingSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+mod_opt_type(roster) -> v_roster();
+mod_opt_type(message) -> v_message();
+mod_opt_type(presence) -> v_presence();
+mod_opt_type(_) ->
+ [roster, message, presence].
+
+depends(_, _) ->
+ [].
+
+-spec component_connected(binary()) -> ok.
+component_connected(Host) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_connected, Host})
+ end, ?MYHOSTS).
+
+-spec component_disconnected(binary(), binary()) -> ok.
+component_disconnected(Host, _Reason) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_disconnected, Host})
+ end, ?MYHOSTS).
+
+-spec process_message(jid(), jid(), stanza()) -> stop | ok.
+process_message(#jid{luser = <<"">>, lresource = <<"">>} = From,
+ #jid{lresource = <<"">>} = To,
+ #message{lang = Lang, type = T} = Msg) when T /= error ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ case proplists:get_value(message, Access, none) of
+ outgoing ->
+ forward_message(From, To, Msg);
+ none ->
+ Txt = <<"Insufficient privilege">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end,
+ stop;
+ error ->
+ %% Component is disconnected
+ ok
+ end;
+process_message(_From, _To, _Stanza) ->
+ ok.
+
+-spec roster_access(boolean(), iq()) -> boolean().
+roster_access(true, _) ->
+ true;
+roster_access(false, #iq{from = From, to = To, type = Type}) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ Permission = proplists:get_value(roster, Access, none),
+ (Permission == both)
+ orelse (Permission == get andalso Type == get)
+ orelse (Permission == set andalso Type == set);
+ error ->
+ %% Component is disconnected
+ false
+ end.
+
+-spec process_presence_out(stanza(), ejabberd_c2s:state(), jid(), jid()) -> stanza().
+process_presence_out(#presence{type = Type} = Pres, _C2SState,
+ #jid{luser = LUser, lserver = LServer} = From,
+ #jid{luser = LUser, lserver = LServer, lresource = <<"">>})
+ when Type == available; Type == unavailable ->
+ %% Self-presence processing
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ Permission = proplists:get_value(presence, Access, none),
+ if Permission == roster; Permission == managed_entity ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_out(Acc, _, _, _) ->
+ Acc.
+
+-spec process_presence_in(stanza(), ejabberd_c2s:state(),
+ jid(), jid(), jid()) -> stanza().
+process_presence_in(#presence{type = Type} = Pres, _C2SState, _,
+ #jid{luser = U, lserver = S} = From,
+ #jid{luser = LUser, lserver = LServer})
+ when {U, S} /= {LUser, LServer} andalso
+ (Type == available orelse Type == unavailable) ->
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ case proplists:get_value(presence, Access, none) of
+ roster ->
+ Permission = proplists:get_value(roster, Access, none),
+ if Permission == both; Permission == get ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end;
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_in(Acc, _, _, _, _) ->
+ Acc.
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([Host, _Opts]) ->
+ ejabberd_hooks:add(component_connected, ?MODULE,
+ component_connected, 50),
+ ejabberd_hooks:add(component_disconnected, ?MODULE,
+ component_disconnected, 50),
+ ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50),
+ {ok, #state{server_host = Host}}.
+
+handle_call(get_permissions, _From, State) ->
+ {reply, {ok, State#state.permissions}, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({component_connected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ From = jid:make(ServerHost),
+ To = jid:make(Host),
+ RosterPerm = get_roster_permission(ServerHost, Host),
+ PresencePerm = get_presence_permission(ServerHost, Host),
+ MessagePerm = get_message_permission(ServerHost, Host),
+ if RosterPerm /= none, PresencePerm /= none, MessagePerm /= none ->
+ Priv = #privilege{perms = [#privilege_perm{access = message,
+ type = MessagePerm},
+ #privilege_perm{access = roster,
+ type = RosterPerm},
+ #privilege_perm{access = presence,
+ type = PresencePerm}]},
+ ?INFO_MSG("Granting permissions to external "
+ "component '~s': roster = ~s, presence = ~s, "
+ "message = ~s",
+ [Host, RosterPerm, PresencePerm, MessagePerm]),
+ Msg = #message{from = From, to = To, sub_els = [Priv]},
+ ejabberd_router:route(From, To, Msg),
+ Permissions = dict:store(Host, [{roster, RosterPerm},
+ {presence, PresencePerm},
+ {message, MessagePerm}],
+ State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+ true ->
+ ?INFO_MSG("Granting no permissions to external component '~s'",
+ [Host]),
+ {noreply, State}
+ end;
+handle_cast({component_disconnected, Host}, State) ->
+ Permissions = dict:erase(Host, State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ %% Note: we don't remove component_* hooks because they are global
+ %% and might be registered within a module on another virtual host
+ Host = State#state.server_host,
+ ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+get_permissions(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ try gen_server:call(Proc, get_permissions) of
+ {ok, Permissions} ->
+ Permissions
+ catch exit:{noproc, _} ->
+ %% No module is loaded for this virtual host
+ dict:new()
+ end.
+
+forward_message(From, To, Msg) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ case xmpp:get_subtag(Msg, #privilege{}) of
+ #privilege{forwarded = #forwarded{sub_els = [#message{} = SubEl]}} ->
+ case SubEl#message.from of
+ #jid{lresource = <<"">>, lserver = ServerHost} ->
+ ejabberd_router:route(
+ xmpp:get_from(SubEl), xmpp:get_to(SubEl), SubEl);
+ _ ->
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid 'from' attribute">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end;
+ _ ->
+ ?ERROR_MSG("got invalid forwarded payload from external "
+ "component '~s':~n~s", [Host, xmpp:pp(Msg)]),
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid forwarded payload">>,
+ Err = xmpp:err_bad_request(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end.
+
+get_roster_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, roster,
+ v_roster(), []),
+ case match_rule(ServerHost, Host, Perms, both) of
+ allow ->
+ both;
+ deny ->
+ Get = match_rule(ServerHost, Host, Perms, get),
+ Set = match_rule(ServerHost, Host, Perms, set),
+ if Get == allow, Set == allow -> both;
+ Get == allow -> get;
+ Set == allow -> set;
+ true -> none
+ end
+ end.
+
+get_message_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, message,
+ v_message(), []),
+ case match_rule(ServerHost, Host, Perms, outgoing) of
+ allow -> outgoing;
+ deny -> none
+ end.
+
+get_presence_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, presence,
+ v_presence(), []),
+ case match_rule(ServerHost, Host, Perms, roster) of
+ allow ->
+ roster;
+ deny ->
+ case match_rule(ServerHost, Host, Perms, managed_entity) of
+ allow -> managed_entity;
+ deny -> none
+ end
+ end.
+
+match_rule(ServerHost, Host, Perms, Type) ->
+ Access = proplists:get_value(Type, Perms, none),
+ acl:match_rule(ServerHost, Access, jid:make(Host)).
+
+v_roster() ->
+ fun(Props) ->
+ lists:map(
+ fun({both, ACL}) -> {both, acl:access_rules_validator(ACL)};
+ ({get, ACL}) -> {get, acl:access_rules_validator(ACL)};
+ ({set, ACL}) -> {set, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_message() ->
+ fun(Props) ->
+ lists:map(
+ fun({outgoing, ACL}) -> {outgoing, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_presence() ->
+ fun(Props) ->
+ lists:map(
+ fun({managed_entity, ACL}) ->
+ {managed_entity, acl:access_rules_validator(ACL)};
+ ({roster, ACL}) ->
+ {roster, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index e3fe64c16..a586935b8 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -385,8 +385,6 @@ depends(ServerHost, Opts) ->
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
-%% <p>The modules are initialized in alphetical order and the list is checked
-%% and sorted to ensure that each module is initialized only once.</p>
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
init_plugins(Host, ServerHost, Opts) ->
TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts,
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index fa27f866c..c344213f3 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -139,15 +139,18 @@ stop(Host) ->
depends(_Host, _Opts) ->
[].
-process_iq(#iq{from = #jid{luser = <<"">>},
- to = #jid{resource = <<"">>}} = IQ) ->
- process_iq_manager(IQ);
process_iq(#iq{from = #jid{luser = U, lserver = S},
to = #jid{luser = U, lserver = S}} = IQ) ->
process_local_iq(IQ);
-process_iq(#iq{lang = Lang} = IQ) ->
- Txt = <<"Query to another users is forbidden">>,
- xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)).
+process_iq(#iq{lang = Lang, to = To} = IQ) ->
+ case ejabberd_hooks:run_fold(roster_remote_access,
+ To#jid.lserver, false, [IQ]) of
+ false ->
+ Txt = <<"Query to another users is forbidden">>,
+ xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
+ true ->
+ process_local_iq(IQ)
+ end.
process_local_iq(#iq{type = set,lang = Lang,
sub_els = [#roster_query{
@@ -251,10 +254,10 @@ write_roster_version(LUser, LServer, InTransaction) ->
%% - roster versioning is not used by the client OR
%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
%% - the roster version from client don't match current version.
-process_iq_get(#iq{from = From, to = To, lang = Lang,
+process_iq_get(#iq{to = To, lang = Lang,
sub_els = [#roster_query{ver = RequestedVersion}]} = IQ) ->
- LUser = From#jid.luser,
- LServer = From#jid.lserver,
+ LUser = To#jid.luser,
+ LServer = To#jid.lserver,
US = {LUser, LServer},
try {ItemsToSend, VersionToSend} =
case {roster_versioning_enabled(LServer),
@@ -303,7 +306,7 @@ process_iq_get(#iq{from = From, to = To, lang = Lang,
end)
catch E:R ->
?ERROR_MSG("failed to process roster get for ~s: ~p",
- [jid:to_string(From), {E, {R, erlang:get_stacktrace()}}]),
+ [jid:to_string(To), {E, {R, erlang:get_stacktrace()}}]),
Txt = <<"Roster module has failed">>,
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end.
@@ -369,10 +372,10 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_roster_by_jid(LUser, LServer, LJID).
-process_iq_set(#iq{from = From, to = To, id = Id,
+process_iq_set(#iq{from = From, to = To,
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
- Managed = is_managed_from_id(Id),
- #jid{user = User, luser = LUser, lserver = LServer} = From,
+ #jid{user = User, luser = LUser, lserver = LServer} = To,
+ Managed = {From#jid.luser, From#jid.lserver} /= {LUser, LServer},
F = fun () ->
lists:map(
fun(#roster_item{jid = JID1} = QueryItem) ->
@@ -397,11 +400,10 @@ process_iq_set(#iq{from = From, to = To, id = Id,
{atomic, ItemPairs} ->
lists:foreach(
fun({OldItem, Item}) ->
- send_itemset_to_managers(From, Item, Managed),
push_item(User, LServer, To, Item),
case Item#roster.subscription of
remove ->
- send_unsubscribing_presence(From, OldItem);
+ send_unsubscribing_presence(To, OldItem);
_ ->
ok
end
@@ -1012,66 +1014,6 @@ webadmin_user(Acc, _User, _Server, Lang) ->
[?XE(<<"h3">>, [?ACT(<<"roster/">>, <<"Roster">>)])].
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
-%% Implement XEP-0321 Remote Roster Management
-
-process_iq_manager(#iq{from = From, to = To, lang = Lang} = IQ) ->
- %% Check what access is allowed for From to To
- MatchDomain = From#jid.lserver,
- case is_domain_managed(MatchDomain, To#jid.lserver) of
- true ->
- process_iq_manager2(MatchDomain, IQ);
- false ->
- Txt = <<"Roster management is not allowed from this domain">>,
- xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
- end.
-
-process_iq_manager2(MatchDomain, #iq{to = To} = IQ) ->
- %% If IQ is SET, filter the input IQ
- IQFiltered = maybe_filter_request(MatchDomain, IQ),
- %% Call the standard function with reversed JIDs
- IdInitial = IQFiltered#iq.id,
- ResIQ = process_iq(IQFiltered#iq{from = To, to = To,
- id = <<"roster-remotely-managed">>}),
- %% Filter the output IQ
- filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}).
-
-is_domain_managed(ContactHost, UserHost) ->
- Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers,
- fun(B) when is_list(B) -> B end,
- []),
- lists:member(ContactHost, Managers).
-
-maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set ->
- filter_stanza(MatchDomain, IQ);
-maybe_filter_request(_MatchDomain, IQ) ->
- IQ.
-
-filter_stanza(MatchDomain,
- #iq{sub_els = [#roster_query{items = Items} = R]} = IQ) ->
- ItemsFiltered = lists:filter(
- fun(#roster_item{jid = #jid{lserver = S}}) ->
- S == MatchDomain
- end, Items),
- IQ#iq{sub_els = [R#roster_query{items = ItemsFiltered}]}.
-
-send_itemset_to_managers(_From, _Item, true) ->
- ok;
-send_itemset_to_managers(From, Item, false) ->
- {_, UserHost} = Item#roster.us,
- {_ContactUser, ContactHost, _ContactResource} = Item#roster.jid,
- %% Check if the component is an allowed manager
- IsManager = is_domain_managed(ContactHost, UserHost),
- case IsManager of
- true -> push_item(<<"">>, ContactHost, <<"">>, From, Item);
- false -> ok
- end.
-
-is_managed_from_id(<<"roster-remotely-managed">>) ->
- true;
-is_managed_from_id(_Id) ->
- false.
-
has_duplicated_groups(Groups) ->
GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
not (length(GroupsPrep) == length(Groups)).
diff --git a/src/mod_stats.erl b/src/mod_stats.erl
index c4b8ddb15..f146498c6 100644
--- a/src/mod_stats.erl
+++ b/src/mod_stats.erl
@@ -127,13 +127,8 @@ get_local_stat(Server, [], Name)
end;
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/online">> ->
- case catch mnesia:table_info(session, size) of
- {'EXIT', _Reason} ->
- ?STATERR(500, <<"Internal Server Error">>);
- Users ->
- ?STATVAL((iolist_to_binary(integer_to_list(Users))),
- <<"users">>)
- end;
+ Users = ejabberd_sm:connected_users_number(),
+ ?STATVAL((iolist_to_binary(integer_to_list(Users))), <<"users">>);
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/total">> ->
NumUsers = lists:foldl(fun (Host, Total) ->
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index c1dfd0e81..5adf1e559 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -664,7 +664,7 @@ get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex,
Before /= undefined -> {<<">">>, <<"asc">>};
true -> {<<"is not">>, <<"desc">>}
end,
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
I = if After /= undefined -> After;
Before /= undefined -> Before;
true -> undefined
@@ -774,7 +774,7 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
get_last_items(Nidx, _From, Count) ->
Limit = jlib:i2l(Count),
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
Query = fun(mssql, _) ->
ejabberd_sql:sql_query_t(
[<<"select top ">>, Limit,
@@ -876,7 +876,7 @@ del_items(Nidx, [ItemId]) ->
del_item(Nidx, ItemId);
del_items(Nidx, ItemIds) ->
I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>),
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
catch
ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>,
I, <<") and nodeid='">>, SNidx, <<"';">>]).
@@ -932,8 +932,9 @@ select_affiliation_subscriptions(Nidx, JID, JID) ->
select_affiliation_subscriptions(Nidx, JID);
select_affiliation_subscriptions(Nidx, GenKey, SubKey) ->
{result, Affiliation} = get_affiliation(Nidx, GenKey),
- {result, Subscriptions} = get_subscriptions(Nidx, SubKey),
- {Affiliation, Subscriptions}.
+ {result, BareJidSubs} = get_subscriptions(Nidx, GenKey),
+ {result, FullJidSubs} = get_subscriptions(Nidx, SubKey),
+ {Affiliation, BareJidSubs++FullJidSubs}.
update_affiliation(Nidx, JID, Affiliation) ->
J = encode_jid(JID),
diff --git a/src/node_mb.erl b/src/node_mb.erl
index 0c3bd3722..c06c08d67 100644
--- a/src/node_mb.erl
+++ b/src/node_mb.erl
@@ -37,6 +37,7 @@
%%% plugins:
%%% - "flat"
%%% - "pep" # Requires mod_caps.
+%%% - "mb"
%%% pep_mapping:
%%% "urn:xmpp:microblog:0": "mb"
%%% </pre></p>
@@ -153,7 +154,7 @@ set_subscriptions(Nidx, Owner, Subscription, SubId) ->
node_pep:set_subscriptions(Nidx, Owner, Subscription, SubId).
get_pending_nodes(Host, Owner) ->
- node_hometree:get_pending_nodes(Host, Owner).
+ node_pep:get_pending_nodes(Host, Owner).
get_states(Nidx) ->
node_pep:get_states(Nidx).
diff --git a/src/node_mb_sql.erl b/src/node_mb_sql.erl
new file mode 100644
index 000000000..125674316
--- /dev/null
+++ b/src/node_mb_sql.erl
@@ -0,0 +1,158 @@
+%%%----------------------------------------------------------------------
+%%% File : node_mb_sql.erl
+%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
+%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend
+%%% Created : 6 Sep 2016 by Holger Weiss <holger@zedat.fu-berlin.de>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(node_mb_sql).
+-behaviour(gen_pubsub_node).
+-author('holger@zedat.fu-berlin.de').
+
+-include("pubsub.hrl").
+-include("jlib.hrl").
+
+-export([init/3, terminate/2, options/0, features/0,
+ create_node_permission/6, create_node/2, delete_node/1,
+ purge_node/2, subscribe_node/8, unsubscribe_node/4,
+ publish_item/7, delete_item/4, remove_extra_items/3,
+ get_entity_affiliations/2, get_node_affiliations/1,
+ get_affiliation/2, set_affiliation/3,
+ get_entity_subscriptions/2, get_node_subscriptions/1,
+ get_subscriptions/2, set_subscriptions/4,
+ get_pending_nodes/2, get_states/1, get_state/2,
+ set_state/1, get_items/7, get_items/3, get_item/7,
+ get_item/2, set_item/1, get_item_name/3, node_to_path/1,
+ path_to_node/1, get_entity_subscriptions_for_send_last/2,
+ get_last_items/3]).
+
+init(Host, ServerHost, Opts) ->
+ node_pep_sql:init(Host, ServerHost, Opts).
+
+terminate(Host, ServerHost) ->
+ node_pep_sql:terminate(Host, ServerHost), ok.
+
+options() ->
+ [{sql, true}, {rsm, true} | node_mb:options()].
+
+features() ->
+ [<<"rsm">> | node_mb:features()].
+
+create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) ->
+ node_pep_sql:create_node_permission(Host, ServerHost, Node, ParentNode,
+ Owner, Access).
+
+create_node(Nidx, Owner) ->
+ node_pep_sql:create_node(Nidx, Owner).
+
+delete_node(Removed) ->
+ node_pep_sql:delete_node(Removed).
+
+subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options) ->
+ node_pep_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options).
+
+unsubscribe_node(Nidx, Sender, Subscriber, SubId) ->
+ node_pep_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId).
+
+publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
+ node_pep_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
+ Payload, PubOpts).
+
+remove_extra_items(Nidx, MaxItems, ItemIds) ->
+ node_pep_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+
+delete_item(Nidx, Publisher, PublishModel, ItemId) ->
+ node_pep_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
+
+purge_node(Nidx, Owner) ->
+ node_pep_sql:purge_node(Nidx, Owner).
+
+get_entity_affiliations(Host, Owner) ->
+ node_pep_sql:get_entity_affiliations(Host, Owner).
+
+get_node_affiliations(Nidx) ->
+ node_pep_sql:get_node_affiliations(Nidx).
+
+get_affiliation(Nidx, Owner) ->
+ node_pep_sql:get_affiliation(Nidx, Owner).
+
+set_affiliation(Nidx, Owner, Affiliation) ->
+ node_pep_sql:set_affiliation(Nidx, Owner, Affiliation).
+
+get_entity_subscriptions(Host, Owner) ->
+ node_pep_sql:get_entity_subscriptions(Host, Owner).
+
+get_entity_subscriptions_for_send_last(Host, Owner) ->
+ node_pep_sql:get_entity_subscriptions_for_send_last(Host, Owner).
+
+get_node_subscriptions(Nidx) ->
+ node_pep_sql:get_node_subscriptions(Nidx).
+
+get_subscriptions(Nidx, Owner) ->
+ node_pep_sql:get_subscriptions(Nidx, Owner).
+
+set_subscriptions(Nidx, Owner, Subscription, SubId) ->
+ node_pep_sql:set_subscriptions(Nidx, Owner, Subscription, SubId).
+
+get_pending_nodes(Host, Owner) ->
+ node_pep_sql:get_pending_nodes(Host, Owner).
+
+get_states(Nidx) ->
+ node_pep_sql:get_states(Nidx).
+
+get_state(Nidx, JID) ->
+ node_pep_sql:get_state(Nidx, JID).
+
+set_state(State) ->
+ node_pep_sql:set_state(State).
+
+get_items(Nidx, From, RSM) ->
+ node_pep_sql:get_items(Nidx, From, RSM).
+
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId,
+ RSM) ->
+ node_pep_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId, RSM).
+
+get_last_items(Nidx, JID, Count) ->
+ node_pep_sql:get_last_items(Nidx, JID, Count).
+
+get_item(Nidx, ItemId) ->
+ node_pep_sql:get_item(Nidx, ItemId).
+
+get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup,
+ SubId) ->
+ node_pep_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId).
+
+set_item(Item) ->
+ node_pep_sql:set_item(Item).
+
+get_item_name(Host, Node, Id) ->
+ node_pep_sql:get_item_name(Host, Node, Id).
+
+node_to_path(Node) ->
+ node_pep_sql:node_to_path(Node).
+
+path_to_node(Path) ->
+ node_pep_sql:path_to_node(Path).
diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl
index c292c7755..5e4462160 100644
--- a/src/nodetree_tree_sql.erl
+++ b/src/nodetree_tree_sql.erl
@@ -77,9 +77,9 @@ set_node(Record) when is_record(Record, pubsub_node) ->
catch
ejabberd_sql:sql_query_t(
?SQL("update pubsub_node set"
- " host=%(H)s"
- " node=%(Node)s"
- " parent=%(Parent)s"
+ " host=%(H)s,"
+ " node=%(Node)s,"
+ " parent=%(Parent)s,"
" type=%(Type)s "
"where nodeid=%(OldNidx)d")),
OldNidx;
diff --git a/src/randoms.erl b/src/randoms.erl
index 60d0b4e3d..1353f48af 100644
--- a/src/randoms.erl
+++ b/src/randoms.erl
@@ -27,14 +27,29 @@
-author('alexey@process-one.net').
--export([get_string/0]).
+-export([get_string/0, uniform/0, uniform/1, bytes/1]).
-export([start/0]).
+-define(THRESHOLD, 16#10000000000000000).
+
start() ->
ok.
get_string() ->
- R = crypto:rand_uniform(0, 16#10000000000000000),
- integer_to_binary(R).
+ R = crypto:rand_uniform(0, ?THRESHOLD),
+ jlib:integer_to_binary(R).
+
+uniform() ->
+ crypto:rand_uniform(0, ?THRESHOLD)/?THRESHOLD.
+
+uniform(N) ->
+ crypto:rand_uniform(1, N+1).
+-ifdef(STRONG_RAND_BYTES).
+bytes(N) ->
+ crypto:strong_rand_bytes(N).
+-else.
+bytes(N) ->
+ crypto:rand_bytes(N).
+-endif.
diff --git a/src/rest.erl b/src/rest.erl
new file mode 100644
index 000000000..01b04f66a
--- /dev/null
+++ b/src/rest.erl
@@ -0,0 +1,181 @@
+%%%----------------------------------------------------------------------
+%%% File : rest.erl
+%%% Author : Christophe Romain <christophe.romain@process-one.net>
+%%% Purpose : Generic REST client
+%%% Created : 16 Oct 2014 by Christophe Romain <christophe.romain@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(rest).
+
+-behaviour(ejabberd_config).
+
+-export([start/1, stop/1, get/2, get/3, post/4, delete/2,
+ request/6, with_retry/4, opt_type/1]).
+
+-include("logger.hrl").
+
+-define(HTTP_TIMEOUT, 10000).
+-define(CONNECT_TIMEOUT, 8000).
+
+start(Host) ->
+ http_p1:start(),
+ Pool_size =
+ ejabberd_config:get_option({ext_api_http_pool_size, Host},
+ fun(X) when is_integer(X), X > 0->
+ X
+ end,
+ 100),
+ http_p1:set_pool_size(Pool_size).
+
+stop(_Host) ->
+ ok.
+
+with_retry(Method, Args, MaxRetries, Backoff) ->
+ with_retry(Method, Args, 0, MaxRetries, Backoff).
+with_retry(Method, Args, Retries, MaxRetries, Backoff) ->
+ case apply(?MODULE, Method, Args) of
+ %% Only retry on timeout errors
+ {error, {http_error,{error,Error}}}
+ when Retries < MaxRetries
+ andalso (Error == 'timeout' orelse Error == 'connect_timeout') ->
+ timer:sleep(round(math:pow(2, Retries)) * Backoff),
+ with_retry(Method, Args, Retries+1, MaxRetries, Backoff);
+ Result ->
+ Result
+ end.
+
+get(Server, Path) ->
+ request(Server, get, Path, [], "application/json", <<>>).
+get(Server, Path, Params) ->
+ request(Server, get, Path, Params, "application/json", <<>>).
+
+delete(Server, Path) ->
+ request(Server, delete, Path, [], "application/json", <<>>).
+
+post(Server, Path, Params, Content) ->
+ Data = case catch jiffy:encode(Content) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("HTTP content encodage failed:~n"
+ "** Content = ~p~n"
+ "** Err = ~p",
+ [Content, Reason]),
+ <<>>;
+ Encoded ->
+ Encoded
+ end,
+ request(Server, post, Path, Params, "application/json", Data).
+
+request(Server, Method, Path, Params, Mime, Data) ->
+ URI = url(Server, Path, Params),
+ Opts = [{connect_timeout, ?CONNECT_TIMEOUT},
+ {timeout, ?HTTP_TIMEOUT}],
+ Hdrs = [{"connection", "keep-alive"},
+ {"content-type", Mime},
+ {"User-Agent", "ejabberd"}],
+ Begin = os:timestamp(),
+ Result = case catch http_p1:request(Method, URI, Hdrs, Data, Opts) of
+ {ok, Code, _, <<>>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<" ">>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<"\r\n">>} ->
+ {ok, Code, []};
+ {ok, Code, _, Body} ->
+ try jiffy:decode(Body) of
+ JSon ->
+ {ok, Code, JSon}
+ catch
+ _:Error ->
+ ?ERROR_MSG("HTTP response decode failed:~n"
+ "** URI = ~s~n"
+ "** Body = ~p~n"
+ "** Err = ~p",
+ [URI, Body, Error]),
+ {error, {invalid_json, Body}}
+ end;
+ {error, Reason} ->
+ ?ERROR_MSG("HTTP request failed:~n"
+ "** URI = ~s~n"
+ "** Err = ~p",
+ [URI, Reason]),
+ {error, {http_error, {error, Reason}}};
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("HTTP request failed:~n"
+ "** URI = ~s~n"
+ "** Err = ~p",
+ [URI, Reason]),
+ {error, {http_error, {error, Reason}}}
+ end,
+ ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]),
+ case Result of
+ {ok, _, _} ->
+ End = os:timestamp(),
+ Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms
+ ejabberd_hooks:run(backend_api_response_time, Server,
+ [Server, Method, Path, Elapsed]);
+ {error, {http_error,{error,timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, {http_error,{error,connect_timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, _} ->
+ ejabberd_hooks:run(backend_api_error, Server,
+ [Server, Method, Path])
+ end,
+ Result.
+
+%%%----------------------------------------------------------------------
+%%% HTTP helpers
+%%%----------------------------------------------------------------------
+
+base_url(Server, Path) ->
+ Tail = case iolist_to_binary(Path) of
+ <<$/, Ok/binary>> -> Ok;
+ Ok -> Ok
+ end,
+ case Tail of
+ <<"http", _Url/binary>> -> Tail;
+ _ ->
+ Base = ejabberd_config:get_option({ext_api_url, Server},
+ fun(X) ->
+ iolist_to_binary(X)
+ end,
+ <<"http://localhost/api">>),
+ <<Base/binary, "/", Tail/binary>>
+ end.
+
+url(Server, Path, []) ->
+ binary_to_list(base_url(Server, Path));
+url(Server, Path, Params) ->
+ Base = base_url(Server, Path),
+ [<<$&, ParHead/binary>> | ParTail] =
+ [<<"&", (iolist_to_binary(Key))/binary, "=",
+ (ejabberd_http:url_encode(Value))/binary>>
+ || {Key, Value} <- Params],
+ Tail = iolist_to_binary([ParHead | ParTail]),
+ binary_to_list(<<Base/binary, $?, Tail/binary>>).
+
+opt_type(ext_api_http_pool_size) ->
+ fun (X) when is_integer(X), X > 0 -> X end;
+opt_type(ext_api_url) ->
+ fun (X) -> iolist_to_binary(X) end;
+opt_type(_) -> [ext_api_http_pool_size, ext_api_url].
diff --git a/src/xmpp_codec.erl b/src/xmpp_codec.erl
index 345de7031..8713365cc 100644
--- a/src/xmpp_codec.erl
+++ b/src/xmpp_codec.erl
@@ -20,6 +20,48 @@ decode({xmlel, _name, _attrs, _} = _el, TopXMLNS,
Opts) ->
IgnoreEls = proplists:get_bool(ignore_els, Opts),
case {_name, get_attr(<<"xmlns">>, _attrs), TopXMLNS} of
+ {<<"query">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegation_query(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"query">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegation_query(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegate">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegate(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegate">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegate(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegation">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegation(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegation">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegation(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegated">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegated(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegated">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegated(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"attribute">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"attribute">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"privilege">>, <<"urn:xmpp:privilege:1">>, _} ->
+ decode_privilege(<<"urn:xmpp:privilege:1">>, IgnoreEls,
+ _el);
+ {<<"privilege">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ decode_privilege(<<"urn:xmpp:privilege:1">>, IgnoreEls,
+ _el);
+ {<<"perm">>, <<"urn:xmpp:privilege:1">>, _} ->
+ decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ IgnoreEls, _el);
+ {<<"perm">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ IgnoreEls, _el);
{<<"thumbnail">>, <<"urn:xmpp:thumbs:1">>, _} ->
decode_thumbnail(<<"urn:xmpp:thumbs:1">>, IgnoreEls,
_el);
@@ -3272,6 +3314,31 @@ decode({xmlel, _name, _attrs, _} = _el, TopXMLNS,
is_known_tag({xmlel, _name, _attrs, _} = _el,
TopXMLNS) ->
case {_name, get_attr(<<"xmlns">>, _attrs), TopXMLNS} of
+ {<<"query">>, <<"urn:xmpp:delegation:1">>, _} -> true;
+ {<<"query">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegate">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegate">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegation">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegation">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegated">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegated">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"attribute">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"attribute">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"privilege">>, <<"urn:xmpp:privilege:1">>, _} ->
+ true;
+ {<<"privilege">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ true;
+ {<<"perm">>, <<"urn:xmpp:privilege:1">>, _} -> true;
+ {<<"perm">>, <<>>, <<"urn:xmpp:privilege:1">>} -> true;
{<<"thumbnail">>, <<"urn:xmpp:thumbs:1">>, _} -> true;
{<<"thumbnail">>, <<>>, <<"urn:xmpp:thumbs:1">>} ->
true;
@@ -5772,7 +5839,17 @@ encode({upload_request, _, _, _, _} = Request,
encode({upload_slot, _, _, _} = Slot, TopXMLNS) ->
encode_upload_slot(Slot, TopXMLNS);
encode({thumbnail, _, _, _, _} = Thumbnail, TopXMLNS) ->
- encode_thumbnail(Thumbnail, TopXMLNS).
+ encode_thumbnail(Thumbnail, TopXMLNS);
+encode({privilege_perm, _, _} = Perm, TopXMLNS) ->
+ encode_privilege_perm(Perm, TopXMLNS);
+encode({privilege, _, _} = Privilege, TopXMLNS) ->
+ encode_privilege(Privilege, TopXMLNS);
+encode({delegated, _, _} = Delegated, TopXMLNS) ->
+ encode_delegated(Delegated, TopXMLNS);
+encode({delegation, _, _} = Delegation, TopXMLNS) ->
+ encode_delegation(Delegation, TopXMLNS);
+encode({delegation_query, _, _} = Query, TopXMLNS) ->
+ encode_delegation_query(Query, TopXMLNS).
get_name({address, _, _, _, _, _}) -> <<"address">>;
get_name({addresses, _}) -> <<"addresses">>;
@@ -5812,6 +5889,9 @@ get_name({db_result, _, _, _, _, _}) -> <<"db:result">>;
get_name({db_verify, _, _, _, _, _, _}) ->
<<"db:verify">>;
get_name({delay, _, _, _}) -> <<"delay">>;
+get_name({delegated, _, _}) -> <<"delegated">>;
+get_name({delegation, _, _}) -> <<"delegation">>;
+get_name({delegation_query, _, _}) -> <<"query">>;
get_name({disco_info, _, _, _, _}) -> <<"query">>;
get_name({disco_item, _, _, _}) -> <<"item">>;
get_name({disco_items, _, _, _}) -> <<"query">>;
@@ -5876,6 +5956,8 @@ get_name({privacy_item, _, _, _, _, _, _, _, _}) ->
get_name({privacy_list, _, _}) -> <<"list">>;
get_name({privacy_query, _, _, _}) -> <<"query">>;
get_name({private, _}) -> <<"query">>;
+get_name({privilege, _, _}) -> <<"privilege">>;
+get_name({privilege_perm, _, _}) -> <<"perm">>;
get_name({ps_affiliation, _, _, _, _}) ->
<<"affiliation">>;
get_name({ps_error, 'closed-node', _}) ->
@@ -6075,6 +6157,12 @@ get_ns({db_result, _, _, _, _, _}) ->
get_ns({db_verify, _, _, _, _, _, _}) ->
<<"jabber:server">>;
get_ns({delay, _, _, _}) -> <<"urn:xmpp:delay">>;
+get_ns({delegated, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
+get_ns({delegation, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
+get_ns({delegation_query, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
get_ns({disco_info, _, _, _, _}) ->
<<"http://jabber.org/protocol/disco#info">>;
get_ns({disco_item, _, _, _}) ->
@@ -6160,6 +6248,9 @@ get_ns({privacy_list, _, _}) -> <<"jabber:iq:privacy">>;
get_ns({privacy_query, _, _, _}) ->
<<"jabber:iq:privacy">>;
get_ns({private, _}) -> <<"jabber:iq:private">>;
+get_ns({privilege, _, _}) -> <<"urn:xmpp:privilege:1">>;
+get_ns({privilege_perm, _, _}) ->
+ <<"urn:xmpp:privilege:1">>;
get_ns({ps_affiliation, Xmlns, _, _, _}) -> Xmlns;
get_ns({ps_error, 'closed-node', _}) ->
<<"http://jabber.org/protocol/pubsub#errors">>;
@@ -6600,6 +6691,11 @@ pp(upload_request, 4) ->
[filename, size, 'content-type', xmlns];
pp(upload_slot, 3) -> [get, put, xmlns];
pp(thumbnail, 4) -> [uri, 'media-type', width, height];
+pp(privilege_perm, 2) -> [access, type];
+pp(privilege, 2) -> [perms, forwarded];
+pp(delegated, 2) -> [ns, attrs];
+pp(delegation, 2) -> [delegated, forwarded];
+pp(delegation_query, 2) -> [to, delegate];
pp(_, _) -> no.
enc_ps_aff(member) -> <<"member">>;
@@ -6704,6 +6800,455 @@ dec_tzo(Val) ->
M = binary_to_integer(M1),
if H >= -12, H =< 12, M >= 0, M < 60 -> {H, M} end.
+decode_delegation_query(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"query">>, _attrs, _els}) ->
+ Delegate = decode_delegation_query_els(__TopXMLNS,
+ __IgnoreEls, _els, []),
+ To = decode_delegation_query_attrs(__TopXMLNS, _attrs,
+ undefined),
+ {delegation_query, To, Delegate}.
+
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls, [],
+ Delegate) ->
+ lists:reverse(Delegate);
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"delegate">>, _attrs, _} = _el | _els],
+ Delegate) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els,
+ [decode_delegate(__TopXMLNS, __IgnoreEls,
+ _el)
+ | Delegate]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els,
+ [decode_delegate(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Delegate]);
+ _ ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els, Delegate)
+ end;
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Delegate) ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els, Delegate).
+
+decode_delegation_query_attrs(__TopXMLNS,
+ [{<<"to">>, _val} | _attrs], _To) ->
+ decode_delegation_query_attrs(__TopXMLNS, _attrs, _val);
+decode_delegation_query_attrs(__TopXMLNS, [_ | _attrs],
+ To) ->
+ decode_delegation_query_attrs(__TopXMLNS, _attrs, To);
+decode_delegation_query_attrs(__TopXMLNS, [], To) ->
+ decode_delegation_query_attr_to(__TopXMLNS, To).
+
+encode_delegation_query({delegation_query, To,
+ Delegate},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els =
+ lists:reverse('encode_delegation_query_$delegate'(Delegate,
+ __NewTopXMLNS, [])),
+ _attrs = encode_delegation_query_attr_to(To,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"query">>, _attrs, _els}.
+
+'encode_delegation_query_$delegate'([], __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_delegation_query_$delegate'([Delegate | _els],
+ __TopXMLNS, _acc) ->
+ 'encode_delegation_query_$delegate'(_els, __TopXMLNS,
+ [encode_delegate(Delegate, __TopXMLNS)
+ | _acc]).
+
+decode_delegation_query_attr_to(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"to">>, <<"query">>, __TopXMLNS}});
+decode_delegation_query_attr_to(__TopXMLNS, _val) ->
+ case catch dec_jid(_val) of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"to">>, <<"query">>, __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_delegation_query_attr_to(_val, _acc) ->
+ [{<<"to">>, enc_jid(_val)} | _acc].
+
+decode_delegate(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegate">>, _attrs, _els}) ->
+ Namespace = decode_delegate_attrs(__TopXMLNS, _attrs,
+ undefined),
+ Namespace.
+
+decode_delegate_attrs(__TopXMLNS,
+ [{<<"namespace">>, _val} | _attrs], _Namespace) ->
+ decode_delegate_attrs(__TopXMLNS, _attrs, _val);
+decode_delegate_attrs(__TopXMLNS, [_ | _attrs],
+ Namespace) ->
+ decode_delegate_attrs(__TopXMLNS, _attrs, Namespace);
+decode_delegate_attrs(__TopXMLNS, [], Namespace) ->
+ decode_delegate_attr_namespace(__TopXMLNS, Namespace).
+
+encode_delegate(Namespace, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_delegate_attr_namespace(Namespace,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"delegate">>, _attrs, _els}.
+
+decode_delegate_attr_namespace(__TopXMLNS, undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"namespace">>, <<"delegate">>,
+ __TopXMLNS}});
+decode_delegate_attr_namespace(__TopXMLNS, _val) ->
+ _val.
+
+encode_delegate_attr_namespace(_val, _acc) ->
+ [{<<"namespace">>, _val} | _acc].
+
+decode_delegation(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegation">>, _attrs, _els}) ->
+ {Forwarded, Delegated} =
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ undefined, []),
+ {delegation, Delegated, Forwarded}.
+
+decode_delegation_els(__TopXMLNS, __IgnoreEls, [],
+ Forwarded, Delegated) ->
+ {Forwarded, lists:reverse(Delegated)};
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"delegated">>, _attrs, _} = _el | _els],
+ Forwarded, Delegated) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded,
+ [decode_delegated(__TopXMLNS, __IgnoreEls, _el)
+ | Delegated]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded,
+ [decode_delegated(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Delegated]);
+ _ ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated)
+ end;
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els],
+ Forwarded, Delegated) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"urn:xmpp:forward:0">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ decode_forwarded(<<"urn:xmpp:forward:0">>,
+ __IgnoreEls, _el),
+ Delegated);
+ _ ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated)
+ end;
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Forwarded, Delegated) ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated).
+
+encode_delegation({delegation, Delegated, Forwarded},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els =
+ lists:reverse('encode_delegation_$forwarded'(Forwarded,
+ __NewTopXMLNS,
+ 'encode_delegation_$delegated'(Delegated,
+ __NewTopXMLNS,
+ []))),
+ _attrs = enc_xmlns_attrs(__NewTopXMLNS, __TopXMLNS),
+ {xmlel, <<"delegation">>, _attrs, _els}.
+
+'encode_delegation_$forwarded'(undefined, __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_delegation_$forwarded'(Forwarded, __TopXMLNS,
+ _acc) ->
+ [encode_forwarded(Forwarded, __TopXMLNS) | _acc].
+
+'encode_delegation_$delegated'([], __TopXMLNS, _acc) ->
+ _acc;
+'encode_delegation_$delegated'([Delegated | _els],
+ __TopXMLNS, _acc) ->
+ 'encode_delegation_$delegated'(_els, __TopXMLNS,
+ [encode_delegated(Delegated, __TopXMLNS)
+ | _acc]).
+
+decode_delegated(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegated">>, _attrs, _els}) ->
+ Attrs = decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ _els, []),
+ Ns = decode_delegated_attrs(__TopXMLNS, _attrs,
+ undefined),
+ {delegated, Ns, Attrs}.
+
+decode_delegated_els(__TopXMLNS, __IgnoreEls, [],
+ Attrs) ->
+ lists:reverse(Attrs);
+decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"attribute">>, _attrs, _} = _el | _els],
+ Attrs) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_delegated_attribute(__TopXMLNS,
+ __IgnoreEls, _el)
+ | Attrs]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Attrs]);
+ _ ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ Attrs)
+ end;
+decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Attrs) ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ Attrs).
+
+decode_delegated_attrs(__TopXMLNS,
+ [{<<"namespace">>, _val} | _attrs], _Ns) ->
+ decode_delegated_attrs(__TopXMLNS, _attrs, _val);
+decode_delegated_attrs(__TopXMLNS, [_ | _attrs], Ns) ->
+ decode_delegated_attrs(__TopXMLNS, _attrs, Ns);
+decode_delegated_attrs(__TopXMLNS, [], Ns) ->
+ decode_delegated_attr_namespace(__TopXMLNS, Ns).
+
+encode_delegated({delegated, Ns, Attrs}, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = lists:reverse('encode_delegated_$attrs'(Attrs,
+ __NewTopXMLNS, [])),
+ _attrs = encode_delegated_attr_namespace(Ns,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"delegated">>, _attrs, _els}.
+
+'encode_delegated_$attrs'([], __TopXMLNS, _acc) -> _acc;
+'encode_delegated_$attrs'([Attrs | _els], __TopXMLNS,
+ _acc) ->
+ 'encode_delegated_$attrs'(_els, __TopXMLNS,
+ [encode_delegated_attribute(Attrs, __TopXMLNS)
+ | _acc]).
+
+decode_delegated_attr_namespace(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"namespace">>, <<"delegated">>,
+ __TopXMLNS}});
+decode_delegated_attr_namespace(__TopXMLNS, _val) ->
+ _val.
+
+encode_delegated_attr_namespace(_val, _acc) ->
+ [{<<"namespace">>, _val} | _acc].
+
+decode_delegated_attribute(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"attribute">>, _attrs, _els}) ->
+ Name = decode_delegated_attribute_attrs(__TopXMLNS,
+ _attrs, undefined),
+ Name.
+
+decode_delegated_attribute_attrs(__TopXMLNS,
+ [{<<"name">>, _val} | _attrs], _Name) ->
+ decode_delegated_attribute_attrs(__TopXMLNS, _attrs,
+ _val);
+decode_delegated_attribute_attrs(__TopXMLNS,
+ [_ | _attrs], Name) ->
+ decode_delegated_attribute_attrs(__TopXMLNS, _attrs,
+ Name);
+decode_delegated_attribute_attrs(__TopXMLNS, [],
+ Name) ->
+ decode_delegated_attribute_attr_name(__TopXMLNS, Name).
+
+encode_delegated_attribute(Name, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_delegated_attribute_attr_name(Name,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"attribute">>, _attrs, _els}.
+
+decode_delegated_attribute_attr_name(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"name">>, <<"attribute">>,
+ __TopXMLNS}});
+decode_delegated_attribute_attr_name(__TopXMLNS,
+ _val) ->
+ _val.
+
+encode_delegated_attribute_attr_name(_val, _acc) ->
+ [{<<"name">>, _val} | _acc].
+
+decode_privilege(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"privilege">>, _attrs, _els}) ->
+ {Perms, Forwarded} = decode_privilege_els(__TopXMLNS,
+ __IgnoreEls, _els, [], undefined),
+ {privilege, Perms, Forwarded}.
+
+decode_privilege_els(__TopXMLNS, __IgnoreEls, [], Perms,
+ Forwarded) ->
+ {lists:reverse(Perms), Forwarded};
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"perm">>, _attrs, _} = _el | _els], Perms,
+ Forwarded) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:privilege:1">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_privilege_perm(__TopXMLNS, __IgnoreEls,
+ _el)
+ | Perms],
+ Forwarded);
+ <<"urn:xmpp:privilege:1">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ __IgnoreEls, _el)
+ | Perms],
+ Forwarded);
+ _ ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded)
+ end;
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els],
+ Perms, Forwarded) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"urn:xmpp:forward:0">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms,
+ decode_forwarded(<<"urn:xmpp:forward:0">>,
+ __IgnoreEls, _el));
+ _ ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded)
+ end;
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Perms, Forwarded) ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded).
+
+encode_privilege({privilege, Perms, Forwarded},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:privilege:1">>, [],
+ __TopXMLNS),
+ _els = lists:reverse('encode_privilege_$perms'(Perms,
+ __NewTopXMLNS,
+ 'encode_privilege_$forwarded'(Forwarded,
+ __NewTopXMLNS,
+ []))),
+ _attrs = enc_xmlns_attrs(__NewTopXMLNS, __TopXMLNS),
+ {xmlel, <<"privilege">>, _attrs, _els}.
+
+'encode_privilege_$perms'([], __TopXMLNS, _acc) -> _acc;
+'encode_privilege_$perms'([Perms | _els], __TopXMLNS,
+ _acc) ->
+ 'encode_privilege_$perms'(_els, __TopXMLNS,
+ [encode_privilege_perm(Perms, __TopXMLNS)
+ | _acc]).
+
+'encode_privilege_$forwarded'(undefined, __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_privilege_$forwarded'(Forwarded, __TopXMLNS,
+ _acc) ->
+ [encode_forwarded(Forwarded, __TopXMLNS) | _acc].
+
+decode_privilege_perm(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"perm">>, _attrs, _els}) ->
+ {Access, Type} = decode_privilege_perm_attrs(__TopXMLNS,
+ _attrs, undefined, undefined),
+ {privilege_perm, Access, Type}.
+
+decode_privilege_perm_attrs(__TopXMLNS,
+ [{<<"access">>, _val} | _attrs], _Access, Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, _val,
+ Type);
+decode_privilege_perm_attrs(__TopXMLNS,
+ [{<<"type">>, _val} | _attrs], Access, _Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, Access,
+ _val);
+decode_privilege_perm_attrs(__TopXMLNS, [_ | _attrs],
+ Access, Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, Access,
+ Type);
+decode_privilege_perm_attrs(__TopXMLNS, [], Access,
+ Type) ->
+ {decode_privilege_perm_attr_access(__TopXMLNS, Access),
+ decode_privilege_perm_attr_type(__TopXMLNS, Type)}.
+
+encode_privilege_perm({privilege_perm, Access, Type},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:privilege:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_privilege_perm_attr_type(Type,
+ encode_privilege_perm_attr_access(Access,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS))),
+ {xmlel, <<"perm">>, _attrs, _els}.
+
+decode_privilege_perm_attr_access(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"access">>, <<"perm">>, __TopXMLNS}});
+decode_privilege_perm_attr_access(__TopXMLNS, _val) ->
+ case catch dec_enum(_val, [roster, message, presence])
+ of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"access">>, <<"perm">>,
+ __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_privilege_perm_attr_access(_val, _acc) ->
+ [{<<"access">>, enc_enum(_val)} | _acc].
+
+decode_privilege_perm_attr_type(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"type">>, <<"perm">>, __TopXMLNS}});
+decode_privilege_perm_attr_type(__TopXMLNS, _val) ->
+ case catch dec_enum(_val,
+ [none, get, set, both, outgoing, roster,
+ managed_entity])
+ of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"type">>, <<"perm">>, __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_privilege_perm_attr_type(_val, _acc) ->
+ [{<<"type">>, enc_enum(_val)} | _acc].
+
decode_thumbnail(__TopXMLNS, __IgnoreEls,
{xmlel, <<"thumbnail">>, _attrs, _els}) ->
{Uri, Media_type, Width, Height} =
diff --git a/src/xmpp_util.erl b/src/xmpp_util.erl
index fb3bbc7ab..7b3e0e892 100644
--- a/src/xmpp_util.erl
+++ b/src/xmpp_util.erl
@@ -92,12 +92,20 @@ has_xdata_var(Var, #xdata{fields = Fields}) ->
-spec make_adhoc_response(adhoc_command(), adhoc_command()) -> adhoc_command().
make_adhoc_response(#adhoc_command{lang = Lang, node = Node, sid = SID},
Command) ->
- Command#adhoc_command{lang = Lang, node = Node, sid = SID}.
+ make_adhoc_response(
+ Command#adhoc_command{lang = Lang, node = Node, sid = SID}).
-spec make_adhoc_response(adhoc_command()) -> adhoc_command().
-make_adhoc_response(#adhoc_command{sid = <<"">>} = Command) ->
+make_adhoc_response(#adhoc_command{sid = <<"">>,
+ status = Status,
+ actions = Actions} = Command) ->
SID = encode_timestamp(p1_time_compat:timestamp()),
- Command#adhoc_command{sid = SID};
+ NewActions = if Actions == undefined, Status /= completed ->
+ #adhoc_actions{execute = complete, complete = true};
+ true ->
+ undefined
+ end,
+ Command#adhoc_command{sid = SID, actions = NewActions};
make_adhoc_response(Command) ->
Command.