Packages
ejabberd
25.3.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_configure.erl
%%%----------------------------------------------------------------------
%%% File : mod_configure.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Support for online configuration of ejabberd using XEP-0050
%%% Created : 19 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2025 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(mod_configure).
-author('alexey@process-one.net').
-protocol({xep, 133, '1.3.0', '13.10', "partial", ""}).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, get_local_identity/5,
get_local_features/5, get_local_items/5,
adhoc_local_items/4, adhoc_local_commands/4,
get_sm_identity/5, get_sm_features/5, get_sm_items/5,
adhoc_sm_items/4, adhoc_sm_commands/4, mod_options/1,
mod_opt_type/1,
depends/2, mod_doc/0]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_sm.hrl").
-include("translate.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
start(_Host, _Opts) ->
{ok, [{hook, disco_local_items, get_local_items, 50},
{hook, disco_local_features, get_local_features, 50},
{hook, disco_local_identity, get_local_identity, 50},
{hook, disco_sm_items, get_sm_items, 50},
{hook, disco_sm_features, get_sm_features, 50},
{hook, disco_sm_identity, get_sm_identity, 50},
{hook, adhoc_local_items, adhoc_local_items, 50},
{hook, adhoc_local_commands, adhoc_local_commands, 50},
{hook, adhoc_sm_items, adhoc_sm_items, 50},
{hook, adhoc_sm_commands, adhoc_sm_commands, 50}]}.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[{mod_adhoc, hard}, {mod_last, soft}].
%%%-----------------------------------------------------------------------
-define(INFO_IDENTITY(Category, Type, Name, Lang),
[#identity{category = Category, type = Type, name = tr(Lang, Name)}]).
-define(INFO_COMMAND(Name, Lang),
?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
Name, Lang)).
-define(NODEJID(To, Name, Node),
#disco_item{jid = To, name = tr(Lang, Name), node = Node}).
-define(NODE(Name, Node),
#disco_item{jid = jid:make(Server),
node = Node,
name = tr(Lang, Name)}).
-define(NS_ADMINX(Sub),
<<(?NS_ADMIN)/binary, "#", Sub/binary>>).
-define(NS_ADMINL(Sub),
[<<"http:">>, <<"jabber.org">>, <<"protocol">>,
<<"admin">>, Sub]).
-spec tokenize(binary()) -> [binary()].
tokenize(Node) -> str:tokens(Node, <<"/#">>).
acl_match_rule(Host, From) ->
Access = mod_configure_opt:access(Host),
acl:match_rule(Host, Access, From).
-spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()].
get_sm_identity(Acc, _From, _To, Node, Lang) ->
case Node of
<<"config">> ->
?INFO_COMMAND(?T("Configuration"), Lang);
_ -> Acc
end.
-spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()].
get_local_identity(Acc, _From, _To, Node, Lang) ->
LNode = tokenize(Node),
case LNode of
[<<"running nodes">>, ENode] ->
?INFO_IDENTITY(<<"ejabberd">>, <<"node">>, ENode, Lang);
[<<"running nodes">>, _ENode, <<"DB">>] ->
?INFO_COMMAND(?T("Database"), Lang);
[<<"running nodes">>, _ENode, <<"backup">>,
<<"backup">>] ->
?INFO_COMMAND(?T("Backup"), Lang);
[<<"running nodes">>, _ENode, <<"backup">>,
<<"restore">>] ->
?INFO_COMMAND(?T("Restore"), Lang);
[<<"running nodes">>, _ENode, <<"backup">>,
<<"textfile">>] ->
?INFO_COMMAND(?T("Dump to Text File"), Lang);
[<<"running nodes">>, _ENode, <<"import">>,
<<"file">>] ->
?INFO_COMMAND(?T("Import File"), Lang);
[<<"running nodes">>, _ENode, <<"import">>,
<<"dir">>] ->
?INFO_COMMAND(?T("Import Directory"), Lang);
[<<"running nodes">>, _ENode, <<"restart">>] ->
?INFO_COMMAND(?T("Restart Service"), Lang);
[<<"running nodes">>, _ENode, <<"shutdown">>] ->
?INFO_COMMAND(?T("Shut Down Service"), Lang);
?NS_ADMINL(<<"add-user">>) ->
?INFO_COMMAND(?T("Add User"), Lang);
?NS_ADMINL(<<"delete-user">>) ->
?INFO_COMMAND(?T("Delete User"), Lang);
?NS_ADMINL(<<"end-user-session">>) ->
?INFO_COMMAND(?T("End User Session"), Lang);
?NS_ADMINL(<<"change-user-password">>) ->
?INFO_COMMAND(?T("Change User Password"), Lang);
?NS_ADMINL(<<"get-user-lastlogin">>) ->
?INFO_COMMAND(?T("Get User Last Login Time"), Lang);
?NS_ADMINL(<<"user-stats">>) ->
?INFO_COMMAND(?T("Get User Statistics"), Lang);
?NS_ADMINL(<<"get-registered-users-list">>) ->
?INFO_COMMAND(?T("Get List of Registered Users"),
Lang);
?NS_ADMINL(<<"get-registered-users-num">>) ->
?INFO_COMMAND(?T("Get Number of Registered Users"),
Lang);
?NS_ADMINL(<<"get-online-users-list">>) ->
?INFO_COMMAND(?T("Get List of Online Users"), Lang);
?NS_ADMINL(<<"get-online-users-num">>) ->
?INFO_COMMAND(?T("Get Number of Online Users"), Lang);
_ -> Acc
end.
%%%-----------------------------------------------------------------------
-define(INFO_RESULT(Allow, Feats, Lang),
case Allow of
deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
allow -> {result, Feats}
end).
-spec get_sm_features(mod_disco:features_acc(), jid(), jid(),
binary(), binary()) -> mod_disco:features_acc().
get_sm_features(Acc, From,
#jid{lserver = LServer} = _To, Node, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false -> Acc;
_ ->
Allow = acl_match_rule(LServer, From),
case Node of
<<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
_ -> Acc
end
end.
-spec get_local_features(mod_disco:features_acc(), jid(), jid(),
binary(), binary()) -> mod_disco:features_acc().
get_local_features(Acc, From,
#jid{lserver = LServer} = _To, Node, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false -> Acc;
_ ->
LNode = tokenize(Node),
Allow = acl_match_rule(LServer, From),
case LNode of
[<<"config">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"user">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"online users">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"all users">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"all users">>, <<$@, _/binary>>] ->
?INFO_RESULT(Allow, [], Lang);
[<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, [], Lang);
[<<"running nodes">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"stopped nodes">>] -> ?INFO_RESULT(Allow, [], Lang);
[<<"running nodes">>, _ENode] ->
?INFO_RESULT(Allow, [?NS_STATS], Lang);
[<<"running nodes">>, _ENode, <<"DB">>] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
[<<"running nodes">>, _ENode, <<"backup">>] ->
?INFO_RESULT(Allow, [], Lang);
[<<"running nodes">>, _ENode, <<"backup">>, _] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
[<<"running nodes">>, _ENode, <<"import">>] ->
?INFO_RESULT(Allow, [], Lang);
[<<"running nodes">>, _ENode, <<"import">>, _] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
[<<"running nodes">>, _ENode, <<"restart">>] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
[<<"running nodes">>, _ENode, <<"shutdown">>] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
[<<"config">>, _] ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"add-user">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"delete-user">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"end-user-session">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"change-user-password">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"get-user-lastlogin">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"user-stats">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"get-registered-users-list">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"get-registered-users-num">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"get-online-users-list">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
?NS_ADMINL(<<"get-online-users-num">>) ->
?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
_ -> Acc
end
end.
%%%-----------------------------------------------------------------------
-spec adhoc_sm_items(mod_disco:items_acc(),
jid(), jid(), binary()) -> mod_disco:items_acc().
adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To,
Lang) ->
case acl_match_rule(LServer, From) of
allow ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
Nodes = [#disco_item{jid = To, node = <<"config">>,
name = tr(Lang, ?T("Configuration"))}],
{result, Items ++ Nodes};
_ -> Acc
end.
%%%-----------------------------------------------------------------------
-spec get_sm_items(mod_disco:items_acc(), jid(), jid(),
binary(), binary()) -> mod_disco:items_acc().
get_sm_items(Acc, From,
#jid{user = User, server = Server, lserver = LServer} =
To,
Node, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false -> Acc;
_ ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
case {acl_match_rule(LServer, From), Node} of
{allow, <<"">>} ->
Nodes = [?NODEJID(To, ?T("Configuration"),
<<"config">>),
?NODEJID(To, ?T("User Management"), <<"user">>)],
{result,
Items ++ Nodes ++ get_user_resources(User, Server)};
{allow, <<"config">>} -> {result, []};
{_, <<"config">>} ->
{error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
_ -> Acc
end
end.
-spec get_user_resources(binary(), binary()) -> [disco_item()].
get_user_resources(User, Server) ->
Rs = ejabberd_sm:get_user_resources(User, Server),
lists:map(fun (R) ->
#disco_item{jid = jid:make(User, Server, R),
name = User}
end,
lists:sort(Rs)).
%%%-----------------------------------------------------------------------
-spec adhoc_local_items(mod_disco:items_acc(),
jid(), jid(), binary()) -> mod_disco:items_acc().
adhoc_local_items(Acc, From,
#jid{lserver = LServer, server = Server} = To, Lang) ->
case acl_match_rule(LServer, From) of
allow ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
PermLev = get_permission_level(From, LServer),
Nodes = recursively_get_local_items(PermLev, LServer,
<<"">>, Server, Lang),
Nodes1 = lists:filter(
fun (#disco_item{node = Nd}) ->
F = get_local_features(empty, From, To, Nd, Lang),
case F of
{result, [?NS_COMMANDS]} -> true;
_ -> false
end
end,
Nodes),
{result, Items ++ Nodes1};
_ -> Acc
end.
-spec recursively_get_local_items(global | vhost, binary(), binary(),
binary(), binary()) -> [disco_item()].
recursively_get_local_items(_PermLev, _LServer,
<<"online users">>, _Server, _Lang) ->
[];
recursively_get_local_items(_PermLev, _LServer,
<<"all users">>, _Server, _Lang) ->
[];
recursively_get_local_items(PermLev, LServer, Node,
Server, Lang) ->
LNode = tokenize(Node),
Items = case get_local_items({PermLev, LServer}, LNode,
Server, Lang)
of
{result, Res} -> Res;
{error, _Error} -> []
end,
lists:flatten(
lists:map(
fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
if (S /= Server) or
(Nd == <<"">>) ->
[];
true ->
[Item,
recursively_get_local_items(
PermLev, LServer, Nd, Server, Lang)]
end
end,
Items)).
-spec get_permission_level(jid(), binary()) -> global | vhost.
get_permission_level(JID, Host) ->
Access = mod_configure_opt:access(Host),
case acl:match_rule(global, Access, JID) of
allow -> global;
deny -> vhost
end.
%%%-----------------------------------------------------------------------
-define(ITEMS_RESULT(Allow, LNode, Fallback),
case Allow of
deny -> Fallback;
allow ->
PermLev = get_permission_level(From, LServer),
case get_local_items({PermLev, LServer}, LNode,
jid:encode(To), Lang)
of
{result, Res} -> {result, Res};
{error, Error} -> {error, Error}
end
end).
-spec get_local_items(mod_disco:items_acc(), jid(), jid(),
binary(), binary()) -> mod_disco:items_acc().
get_local_items(Acc, From, #jid{lserver = LServer} = To,
<<"">>, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false -> Acc;
_ ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
Allow = acl_match_rule(LServer, From),
case Allow of
deny -> {result, Items};
allow ->
PermLev = get_permission_level(From, LServer),
case get_local_items({PermLev, LServer}, [],
jid:encode(To), Lang)
of
{result, Res} -> {result, Items ++ Res};
{error, _Error} -> {result, Items}
end
end
end;
get_local_items(Acc, From, #jid{lserver = LServer} = To,
Node, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false -> Acc;
_ ->
LNode = tokenize(Node),
Allow = acl_match_rule(LServer, From),
Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang),
case LNode of
[<<"config">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"user">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"online users">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"all users">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"all users">>, <<$@, _/binary>>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"outgoing s2s">> | _] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"stopped nodes">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"DB">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"backup">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"backup">>, _] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"import">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"import">>, _] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"restart">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"running nodes">>, _ENode, <<"shutdown">>] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
[<<"config">>, _] ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"add-user">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"delete-user">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"end-user-session">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"change-user-password">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"get-user-lastlogin">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"user-stats">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"get-registered-users-list">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"get-registered-users-num">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"get-online-users-list">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
?NS_ADMINL(<<"get-online-users-num">>) ->
?ITEMS_RESULT(Allow, LNode, {error, Err});
_ -> Acc
end
end.
%%%-----------------------------------------------------------------------
-spec get_local_items({global | vhost, binary()}, [binary()],
binary(), binary()) -> {result, [disco_item()]} | {error, stanza_error()}.
get_local_items(_Host, [], Server, Lang) ->
{result,
[?NODE(?T("Configuration"), <<"config">>),
?NODE(?T("User Management"), <<"user">>),
?NODE(?T("Online Users"), <<"online users">>),
?NODE(?T("All Users"), <<"all users">>),
?NODE(?T("Outgoing s2s Connections"),
<<"outgoing s2s">>),
?NODE(?T("Running Nodes"), <<"running nodes">>),
?NODE(?T("Stopped Nodes"), <<"stopped nodes">>)]};
get_local_items(_Host, [<<"config">>, _], _Server,
_Lang) ->
{result, []};
get_local_items(_Host, [<<"user">>], Server, Lang) ->
{result,
[?NODE(?T("Add User"), (?NS_ADMINX(<<"add-user">>))),
?NODE(?T("Delete User"),
(?NS_ADMINX(<<"delete-user">>))),
?NODE(?T("End User Session"),
(?NS_ADMINX(<<"end-user-session">>))),
?NODE(?T("Change User Password"),
(?NS_ADMINX(<<"change-user-password">>))),
?NODE(?T("Get User Last Login Time"),
(?NS_ADMINX(<<"get-user-lastlogin">>))),
?NODE(?T("Get User Statistics"),
(?NS_ADMINX(<<"user-stats">>))),
?NODE(?T("Get List of Registered Users"),
(?NS_ADMINX(<<"get-registered-users-list">>))),
?NODE(?T("Get Number of Registered Users"),
(?NS_ADMINX(<<"get-registered-users-num">>))),
?NODE(?T("Get List of Online Users"),
(?NS_ADMINX(<<"get-online-users-list">>))),
?NODE(?T("Get Number of Online Users"),
(?NS_ADMINX(<<"get-online-users-num">>)))]};
get_local_items(_Host, [<<"http:">> | _], _Server,
_Lang) ->
{result, []};
get_local_items({_, Host}, [<<"online users">>],
_Server, _Lang) ->
{result, get_online_vh_users(Host)};
get_local_items({_, Host}, [<<"all users">>], _Server,
_Lang) ->
{result, get_all_vh_users(Host)};
get_local_items({_, Host},
[<<"all users">>, <<$@, Diap/binary>>], _Server,
_Lang) ->
Users = ejabberd_auth:get_users(Host),
SUsers = lists:sort([{S, U} || {U, S} <- Users]),
try
[S1, S2] = ejabberd_regexp:split(Diap, <<"-">>),
N1 = binary_to_integer(S1),
N2 = binary_to_integer(S2),
Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
{result, lists:map(
fun({S, U}) ->
#disco_item{jid = jid:make(U, S),
name = <<U/binary, $@, S/binary>>}
end, Sub)}
catch _:_ ->
{error, xmpp:err_not_acceptable()}
end;
get_local_items({_, Host}, [<<"outgoing s2s">>],
_Server, Lang) ->
{result, get_outgoing_s2s(Host, Lang)};
get_local_items({_, Host}, [<<"outgoing s2s">>, To],
_Server, Lang) ->
{result, get_outgoing_s2s(Host, Lang, To)};
get_local_items(_Host, [<<"running nodes">>], Server,
Lang) ->
{result, get_running_nodes(Server, Lang)};
get_local_items(_Host, [<<"stopped nodes">>], _Server,
Lang) ->
{result, get_stopped_nodes(Lang)};
get_local_items({global, _Host},
[<<"running nodes">>, ENode], Server, Lang) ->
{result,
[?NODE(?T("Database"),
<<"running nodes/", ENode/binary, "/DB">>),
?NODE(?T("Backup Management"),
<<"running nodes/", ENode/binary, "/backup">>),
?NODE(?T("Import Users From jabberd14 Spool Files"),
<<"running nodes/", ENode/binary, "/import">>),
?NODE(?T("Restart Service"),
<<"running nodes/", ENode/binary, "/restart">>),
?NODE(?T("Shut Down Service"),
<<"running nodes/", ENode/binary, "/shutdown">>)]};
get_local_items(_Host,
[<<"running nodes">>, _ENode, <<"DB">>], _Server,
_Lang) ->
{result, []};
get_local_items(_Host,
[<<"running nodes">>, ENode, <<"backup">>], Server,
Lang) ->
{result,
[?NODE(?T("Backup"),
<<"running nodes/", ENode/binary, "/backup/backup">>),
?NODE(?T("Restore"),
<<"running nodes/", ENode/binary, "/backup/restore">>),
?NODE(?T("Dump to Text File"),
<<"running nodes/", ENode/binary,
"/backup/textfile">>)]};
get_local_items(_Host,
[<<"running nodes">>, _ENode, <<"backup">>, _], _Server,
_Lang) ->
{result, []};
get_local_items(_Host,
[<<"running nodes">>, ENode, <<"import">>], Server,
Lang) ->
{result,
[?NODE(?T("Import File"),
<<"running nodes/", ENode/binary, "/import/file">>),
?NODE(?T("Import Directory"),
<<"running nodes/", ENode/binary, "/import/dir">>)]};
get_local_items(_Host,
[<<"running nodes">>, _ENode, <<"import">>, _], _Server,
_Lang) ->
{result, []};
get_local_items(_Host,
[<<"running nodes">>, _ENode, <<"restart">>], _Server,
_Lang) ->
{result, []};
get_local_items(_Host,
[<<"running nodes">>, _ENode, <<"shutdown">>], _Server,
_Lang) ->
{result, []};
get_local_items(_Host, _, _Server, _Lang) ->
{error, xmpp:err_item_not_found()}.
-spec get_online_vh_users(binary()) -> [disco_item()].
get_online_vh_users(Host) ->
USRs = ejabberd_sm:get_vh_session_list(Host),
SURs = lists:sort([{S, U, R} || {U, S, R} <- USRs]),
lists:map(
fun({S, U, R}) ->
#disco_item{jid = jid:make(U, S, R),
name = <<U/binary, "@", S/binary>>}
end, SURs).
-spec get_all_vh_users(binary()) -> [disco_item()].
get_all_vh_users(Host) ->
Users = ejabberd_auth:get_users(Host),
SUsers = lists:sort([{S, U} || {U, S} <- Users]),
case length(SUsers) of
N when N =< 100 ->
lists:map(fun({S, U}) ->
#disco_item{jid = jid:make(U, S),
name = <<U/binary, $@, S/binary>>}
end, SUsers);
N ->
NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1,
M = trunc(N / NParts) + 1,
lists:map(
fun (K) ->
L = K + M - 1,
Node = <<"@",
(integer_to_binary(K))/binary,
"-",
(integer_to_binary(L))/binary>>,
{FS, FU} = lists:nth(K, SUsers),
{LS, LU} = if L < N -> lists:nth(L, SUsers);
true -> lists:last(SUsers)
end,
Name = <<FU/binary, "@", FS/binary, " -- ",
LU/binary, "@", LS/binary>>,
#disco_item{jid = jid:make(Host),
node = <<"all users/", Node/binary>>,
name = Name}
end, lists:seq(1, N, M))
end.
-spec get_outgoing_s2s(binary(), binary()) -> [disco_item()].
get_outgoing_s2s(Host, Lang) ->
Connections = ejabberd_s2s:dirty_get_connections(),
DotHost = <<".", Host/binary>>,
TConns = [TH || {FH, TH} <- Connections,
Host == FH orelse str:suffix(DotHost, FH)],
lists:map(
fun (T) ->
Name = str:translate_and_format(Lang, ?T("To ~ts"),[T]),
#disco_item{jid = jid:make(Host),
node = <<"outgoing s2s/", T/binary>>,
name = Name}
end, lists:usort(TConns)).
-spec get_outgoing_s2s(binary(), binary(), binary()) -> [disco_item()].
get_outgoing_s2s(Host, Lang, To) ->
Connections = ejabberd_s2s:dirty_get_connections(),
lists:map(
fun ({F, _T}) ->
Node = <<"outgoing s2s/", To/binary, "/", F/binary>>,
Name = str:translate_and_format(Lang, ?T("From ~ts"), [F]),
#disco_item{jid = jid:make(Host), node = Node, name = Name}
end,
lists:keysort(
1,
lists:filter(fun (E) -> element(2, E) == To end,
Connections))).
-spec get_running_nodes(binary(), binary()) -> [disco_item()].
get_running_nodes(Server, _Lang) ->
DBNodes = mnesia:system_info(running_db_nodes),
lists:map(
fun (N) ->
S = iolist_to_binary(atom_to_list(N)),
#disco_item{jid = jid:make(Server),
node = <<"running nodes/", S/binary>>,
name = S}
end, lists:sort(DBNodes)).
-spec get_stopped_nodes(binary()) -> [disco_item()].
get_stopped_nodes(_Lang) ->
DBNodes = lists:usort(mnesia:system_info(db_nodes) ++
mnesia:system_info(extra_db_nodes))
-- mnesia:system_info(running_db_nodes),
lists:map(
fun (N) ->
S = iolist_to_binary(atom_to_list(N)),
#disco_item{jid = jid:make(ejabberd_config:get_myname()),
node = <<"stopped nodes/", S/binary>>,
name = S}
end, lists:sort(DBNodes)).
%%-------------------------------------------------------------------------
-define(COMMANDS_RESULT(LServerOrGlobal, From, To,
Request, Lang),
case acl_match_rule(LServerOrGlobal, From) of
deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
allow -> adhoc_local_commands(From, To, Request)
end).
-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) ->
adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(Acc, From,
#jid{lserver = LServer} = To,
#adhoc_command{node = Node, lang = Lang} = Request) ->
LNode = tokenize(Node),
case LNode of
[<<"running nodes">>, _ENode, <<"DB">>] ->
?COMMANDS_RESULT(global, From, To, Request, Lang);
[<<"running nodes">>, _ENode, <<"backup">>, _] ->
?COMMANDS_RESULT(global, From, To, Request, Lang);
[<<"running nodes">>, _ENode, <<"import">>, _] ->
?COMMANDS_RESULT(global, From, To, Request, Lang);
[<<"running nodes">>, _ENode, <<"restart">>] ->
?COMMANDS_RESULT(global, From, To, Request, Lang);
[<<"running nodes">>, _ENode, <<"shutdown">>] ->
?COMMANDS_RESULT(global, From, To, Request, Lang);
[<<"config">>, _] ->
?COMMANDS_RESULT(LServer, From, To, Request, Lang);
?NS_ADMINL(_) ->
?COMMANDS_RESULT(LServer, From, To, Request, Lang);
_ -> Acc
end.
-spec adhoc_local_commands(jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(From,
#jid{lserver = LServer} = _To,
#adhoc_command{lang = Lang, node = Node,
sid = SessionID, action = Action,
xdata = XData} = Request) ->
LNode = tokenize(Node),
ActionIsExecute = Action == execute orelse Action == complete,
if Action == cancel ->
#adhoc_command{status = canceled, lang = Lang,
node = Node, sid = SessionID};
XData == undefined, ActionIsExecute ->
case get_form(LServer, LNode, Lang) of
{result, Form} ->
xmpp_util:make_adhoc_response(
Request,
#adhoc_command{status = executing, xdata = Form});
{result, Status, Form} ->
xmpp_util:make_adhoc_response(
Request,
#adhoc_command{status = Status, xdata = Form});
{error, Error} -> {error, Error}
end;
XData /= undefined, ActionIsExecute ->
case set_form(From, LServer, LNode, Lang, XData) of
{result, Res} ->
xmpp_util:make_adhoc_response(
Request,
#adhoc_command{xdata = Res, status = completed});
%%{'EXIT', _} -> {error, xmpp:err_bad_request()};
{error, Error} -> {error, Error}
end;
true ->
{error, xmpp:err_bad_request(?T("Unexpected action"), Lang)}
end.
-define(TVFIELD(Type, Var, Val),
#xdata_field{type = Type, var = Var, values = [Val]}).
-define(HFIELD(),
?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))).
-define(TLFIELD(Type, Label, Var),
#xdata_field{type = Type, label = tr(Lang, Label), var = Var}).
-define(XFIELD(Type, Label, Var, Val),
#xdata_field{type = Type, label = tr(Lang, Label),
var = Var, values = [Val]}).
-define(XMFIELD(Type, Label, Var, Vals),
#xdata_field{type = Type, label = tr(Lang, Label),
var = Var, values = Vals}).
-define(TABLEFIELD(Table, Val),
#xdata_field{
type = 'list-single',
label = iolist_to_binary(atom_to_list(Table)),
var = iolist_to_binary(atom_to_list(Table)),
values = [iolist_to_binary(atom_to_list(Val))],
options = [#xdata_option{label = tr(Lang, ?T("RAM copy")),
value = <<"ram_copies">>},
#xdata_option{label = tr(Lang, ?T("RAM and disc copy")),
value = <<"disc_copies">>},
#xdata_option{label = tr(Lang, ?T("Disc only copy")),
value = <<"disc_only_copies">>},
#xdata_option{label = tr(Lang, ?T("Remote copy")),
value = <<"unknown">>}]}).
-spec get_form(binary(), [binary()], binary()) -> {result, xdata()} |
{result, completed, xdata()} |
{error, stanza_error()}.
get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>],
Lang) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of
{badrpc, Reason} ->
?ERROR_MSG("RPC call mnesia:system_info(tables) on node "
"~ts failed: ~p", [Node, Reason]),
{error, xmpp:err_internal_server_error()};
Tables ->
STables = lists:sort(Tables),
Title = <<(tr(Lang, ?T("Database Tables Configuration at ")))/binary,
ENode/binary>>,
Instr = tr(Lang, ?T("Choose storage type of tables")),
try
Fs = lists:map(
fun(Table) ->
case ejabberd_cluster:call(
Node, mnesia, table_info,
[Table, storage_type]) of
Type when is_atom(Type) ->
?TABLEFIELD(Table, Type)
end
end, STables),
{result, #xdata{title = Title,
type = form,
instructions = [Instr],
fields = [?HFIELD()|Fs]}}
catch _:{case_clause, {badrpc, Reason}} ->
?ERROR_MSG("RPC call mnesia:table_info/2 "
"on node ~ts failed: ~p", [Node, Reason]),
{error, xmpp:err_internal_server_error()}
end
end
end;
get_form(_Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"backup">>],
Lang) ->
{result,
#xdata{title = <<(tr(Lang, ?T("Backup to File at ")))/binary, ENode/binary>>,
type = form,
instructions = [tr(Lang, ?T("Enter path to backup file"))],
fields = [?HFIELD(),
?XFIELD('text-single', ?T("Path to File"),
<<"path">>, <<"">>)]}};
get_form(_Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"restore">>],
Lang) ->
{result,
#xdata{title = <<(tr(Lang, ?T("Restore Backup from File at ")))/binary,
ENode/binary>>,
type = form,
instructions = [tr(Lang, ?T("Enter path to backup file"))],
fields = [?HFIELD(),
?XFIELD('text-single', ?T("Path to File"),
<<"path">>, <<"">>)]}};
get_form(_Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"textfile">>],
Lang) ->
{result,
#xdata{title = <<(tr(Lang, ?T("Dump Backup to Text File at ")))/binary,
ENode/binary>>,
type = form,
instructions = [tr(Lang, ?T("Enter path to text file"))],
fields = [?HFIELD(),
?XFIELD('text-single', ?T("Path to File"),
<<"path">>, <<"">>)]}};
get_form(_Host,
[<<"running nodes">>, ENode, <<"import">>, <<"file">>],
Lang) ->
{result,
#xdata{title = <<(tr(Lang, ?T("Import User from File at ")))/binary,
ENode/binary>>,
type = form,
instructions = [tr(Lang, ?T("Enter path to jabberd14 spool file"))],
fields = [?HFIELD(),
?XFIELD('text-single', ?T("Path to File"),
<<"path">>, <<"">>)]}};
get_form(_Host,
[<<"running nodes">>, ENode, <<"import">>, <<"dir">>],
Lang) ->
{result,
#xdata{title = <<(tr(Lang, ?T("Import Users from Dir at ")))/binary,
ENode/binary>>,
type = form,
instructions = [tr(Lang, ?T("Enter path to jabberd14 spool dir"))],
fields = [?HFIELD(),
?XFIELD('text-single', ?T("Path to Dir"),
<<"path">>, <<"">>)]}};
get_form(_Host,
[<<"running nodes">>, _ENode, <<"restart">>], Lang) ->
Make_option =
fun (LabelNum, LabelUnit, Value) ->
#xdata_option{
label = <<LabelNum/binary, (tr(Lang, LabelUnit))/binary>>,
value = Value}
end,
{result,
#xdata{title = tr(Lang, ?T("Restart Service")),
type = form,
fields = [?HFIELD(),
#xdata_field{
type = 'list-single',
label = tr(Lang, ?T("Time delay")),
var = <<"delay">>,
required = true,
options =
[Make_option(<<"">>, <<"immediately">>, <<"1">>),
Make_option(<<"15 ">>, <<"seconds">>, <<"15">>),
Make_option(<<"30 ">>, <<"seconds">>, <<"30">>),
Make_option(<<"60 ">>, <<"seconds">>, <<"60">>),
Make_option(<<"90 ">>, <<"seconds">>, <<"90">>),
Make_option(<<"2 ">>, <<"minutes">>, <<"120">>),
Make_option(<<"3 ">>, <<"minutes">>, <<"180">>),
Make_option(<<"4 ">>, <<"minutes">>, <<"240">>),
Make_option(<<"5 ">>, <<"minutes">>, <<"300">>),
Make_option(<<"10 ">>, <<"minutes">>, <<"600">>),
Make_option(<<"15 ">>, <<"minutes">>, <<"900">>),
Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]},
#xdata_field{type = fixed,
label = tr(Lang,
?T("Send announcement to all online users "
"on all hosts"))},
#xdata_field{var = <<"subject">>,
type = 'text-single',
label = tr(Lang, ?T("Subject"))},
#xdata_field{var = <<"announcement">>,
type = 'text-multi',
label = tr(Lang, ?T("Message body"))}]}};
get_form(_Host,
[<<"running nodes">>, _ENode, <<"shutdown">>], Lang) ->
Make_option =
fun (LabelNum, LabelUnit, Value) ->
#xdata_option{
label = <<LabelNum/binary, (tr(Lang, LabelUnit))/binary>>,
value = Value}
end,
{result,
#xdata{title = tr(Lang, ?T("Shut Down Service")),
type = form,
fields = [?HFIELD(),
#xdata_field{
type = 'list-single',
label = tr(Lang, ?T("Time delay")),
var = <<"delay">>,
required = true,
options =
[Make_option(<<"">>, <<"immediately">>, <<"1">>),
Make_option(<<"15 ">>, <<"seconds">>, <<"15">>),
Make_option(<<"30 ">>, <<"seconds">>, <<"30">>),
Make_option(<<"60 ">>, <<"seconds">>, <<"60">>),
Make_option(<<"90 ">>, <<"seconds">>, <<"90">>),
Make_option(<<"2 ">>, <<"minutes">>, <<"120">>),
Make_option(<<"3 ">>, <<"minutes">>, <<"180">>),
Make_option(<<"4 ">>, <<"minutes">>, <<"240">>),
Make_option(<<"5 ">>, <<"minutes">>, <<"300">>),
Make_option(<<"10 ">>, <<"minutes">>, <<"600">>),
Make_option(<<"15 ">>, <<"minutes">>, <<"900">>),
Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]},
#xdata_field{type = fixed,
label = tr(Lang,
?T("Send announcement to all online users "
"on all hosts"))},
#xdata_field{var = <<"subject">>,
type = 'text-single',
label = tr(Lang, ?T("Subject"))},
#xdata_field{var = <<"announcement">>,
type = 'text-multi',
label = tr(Lang, ?T("Message body"))}]}};
get_form(_Host, ?NS_ADMINL(<<"add-user">>), Lang) ->
{result,
#xdata{title = tr(Lang, ?T("Add User")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-single',
label = tr(Lang, ?T("Jabber ID")),
required = true,
var = <<"accountjid">>},
#xdata_field{type = 'text-private',
label = tr(Lang, ?T("Password")),
required = true,
var = <<"password">>},
#xdata_field{type = 'text-private',
label = tr(Lang, ?T("Password Verification")),
required = true,
var = <<"password-verify">>}]}};
get_form(_Host, ?NS_ADMINL(<<"delete-user">>), Lang) ->
{result,
#xdata{title = tr(Lang, ?T("Delete User")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-multi',
label = tr(Lang, ?T("Jabber ID")),
required = true,
var = <<"accountjids">>}]}};
get_form(_Host, ?NS_ADMINL(<<"end-user-session">>),
Lang) ->
{result,
#xdata{title = tr(Lang, ?T("End User Session")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-single',
label = tr(Lang, ?T("Jabber ID")),
required = true,
var = <<"accountjid">>}]}};
get_form(_Host, ?NS_ADMINL(<<"change-user-password">>),
Lang) ->
{result,
#xdata{title = tr(Lang, ?T("Change User Password")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-single',
label = tr(Lang, ?T("Jabber ID")),
required = true,
var = <<"accountjid">>},
#xdata_field{type = 'text-private',
label = tr(Lang, ?T("Password")),
required = true,
var = <<"password">>}]}};
get_form(_Host, ?NS_ADMINL(<<"get-user-lastlogin">>),
Lang) ->
{result,
#xdata{title = tr(Lang, ?T("Get User Last Login Time")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-single',
label = tr(Lang, ?T("Jabber ID")),
var = <<"accountjid">>,
required = true}]}};
get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) ->
{result,
#xdata{title = tr(Lang, ?T("Get User Statistics")),
type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-single',
label = tr(Lang, ?T("Jabber ID")),
var = <<"accountjid">>,
required = true}]}};
get_form(Host, ?NS_ADMINL(<<"get-registered-users-list">>), Lang) ->
Values = [jid:encode(jid:make(U, Host))
|| {U, _} <- ejabberd_auth:get_users(Host)],
{result, completed,
#xdata{type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-multi',
label = tr(Lang, ?T("The list of all users")),
var = <<"registereduserjids">>,
values = Values}]}};
get_form(Host,
?NS_ADMINL(<<"get-registered-users-num">>), Lang) ->
Num = integer_to_binary(ejabberd_auth:count_users(Host)),
{result, completed,
#xdata{type = form,
fields = [?HFIELD(),
#xdata_field{type = 'text-single',
label = tr(Lang, ?T("Number of registered users")),
var = <<"registeredusersnum">>,
values = [Num]}]}};
get_form(Host, ?NS_ADMINL(<<"get-online-users-list">>), Lang) ->
Accounts = [jid:encode(jid:make(U, Host))
|| {U, _, _} <- ejabberd_sm:get_vh_session_list(Host)],
Values = lists:usort(Accounts),
{result, completed,
#xdata{type = form,
fields = [?HFIELD(),
#xdata_field{type = 'jid-multi',
label = tr(Lang, ?T("The list of all online users")),
var = <<"onlineuserjids">>,
values = Values}]}};
get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>),
Lang) ->
Num = integer_to_binary(ejabberd_sm:get_vh_session_number(Host)),
{result, completed,
#xdata{type = form,
fields = [?HFIELD(),
#xdata_field{type = 'text-single',
label = tr(Lang, ?T("Number of online users")),
var = <<"onlineusersnum">>,
values = [Num]}]}};
get_form(_Host, _, _Lang) ->
{error, xmpp:err_service_unavailable()}.
-spec set_form(jid(), binary(), [binary()], binary(), xdata()) -> {result, xdata() | undefined} |
{error, stanza_error()}.
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"DB">>], Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
lists:foreach(
fun(#xdata_field{var = SVar, values = SVals}) ->
Table = misc:binary_to_atom(SVar),
Type = case SVals of
[<<"unknown">>] -> unknown;
[<<"ram_copies">>] -> ram_copies;
[<<"disc_copies">>] -> disc_copies;
[<<"disc_only_copies">>] -> disc_only_copies;
_ -> false
end,
if Type == false -> ok;
Type == unknown ->
mnesia:del_table_copy(Table, Node);
true ->
case mnesia:add_table_copy(Table, Node, Type) of
{aborted, _} ->
mnesia:change_table_copy_type(
Table, Node, Type);
_ -> ok
end
end
end, XData#xdata.fields),
{result, undefined}
end;
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"backup">>],
Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case xmpp_util:get_xdata_values(<<"path">>, XData) of
[] ->
Txt = ?T("No 'path' found in data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
[String] ->
case ejabberd_cluster:call(
Node, mnesia, backup, [binary_to_list(String)],
timer:minutes(10)) of
{badrpc, Reason} ->
?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts "
"failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
{error, Reason} ->
?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts "
"failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
_ ->
{result, undefined}
end;
_ ->
Txt = ?T("Incorrect value of 'path' in data form"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"restore">>],
Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case xmpp_util:get_xdata_values(<<"path">>, XData) of
[] ->
Txt = ?T("No 'path' found in data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
[String] ->
case ejabberd_cluster:call(
Node, ejabberd_admin, restore,
[String], timer:minutes(10)) of
{badrpc, Reason} ->
?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node "
"~ts failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
{error, Reason} ->
?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node "
"~ts failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
_ ->
{result, undefined}
end;
_ ->
Txt = ?T("Incorrect value of 'path' in data form"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"backup">>,
<<"textfile">>],
Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case xmpp_util:get_xdata_values(<<"path">>, XData) of
[] ->
Txt = ?T("No 'path' found in data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
[String] ->
case ejabberd_cluster:call(
Node, ejabberd_admin, dump_to_textfile,
[String], timer:minutes(10)) of
{badrpc, Reason} ->
?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) "
"to node ~ts failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
{error, Reason} ->
?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) "
"to node ~ts failed: ~p", [String, Node, Reason]),
{error, xmpp:err_internal_server_error()};
_ ->
{result, undefined}
end;
_ ->
Txt = ?T("Incorrect value of 'path' in data form"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"import">>, <<"file">>],
Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case xmpp_util:get_xdata_values(<<"path">>, XData) of
[] ->
Txt = ?T("No 'path' found in data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
[String] ->
ejabberd_cluster:call(Node, jd2ejd, import_file, [String]),
{result, undefined};
_ ->
Txt = ?T("Incorrect value of 'path' in data form"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
set_form(_From, _Host,
[<<"running nodes">>, ENode, <<"import">>, <<"dir">>],
Lang, XData) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
case xmpp_util:get_xdata_values(<<"path">>, XData) of
[] ->
Txt = ?T("No 'path' found in data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
[String] ->
ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]),
{result, undefined};
_ ->
Txt = ?T("Incorrect value of 'path' in data form"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
set_form(From, Host,
[<<"running nodes">>, ENode, <<"restart">>], _Lang,
XData) ->
stop_node(From, Host, ENode, restart, XData);
set_form(From, Host,
[<<"running nodes">>, ENode, <<"shutdown">>], _Lang,
XData) ->
stop_node(From, Host, ENode, stop, XData);
set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang,
XData) ->
AccountString = get_value(<<"accountjid">>, XData),
Password = get_value(<<"password">>, XData),
Password = get_value(<<"password-verify">>, XData),
AccountJID = jid:decode(AccountString),
User = AccountJID#jid.luser,
Server = AccountJID#jid.lserver,
true = lists:member(Server, ejabberd_option:hosts()),
true = Server == Host orelse
get_permission_level(From, Host) == global,
case ejabberd_auth:try_register(User, Server, Password) of
ok -> {result, undefined};
{error, exists} -> {error, xmpp:err_conflict()};
{error, not_allowed} -> {error, xmpp:err_not_allowed()}
end;
set_form(From, Host, ?NS_ADMINL(<<"delete-user">>),
_Lang, XData) ->
AccountStringList = get_values(<<"accountjids">>,
XData),
[_ | _] = AccountStringList,
ASL2 = lists:map(fun (AccountString) ->
JID = jid:decode(AccountString),
User = JID#jid.luser,
Server = JID#jid.lserver,
true = Server == Host orelse
get_permission_level(From, Host) == global,
true = ejabberd_auth:user_exists(User, Server),
{User, Server}
end,
AccountStringList),
[ejabberd_auth:remove_user(User, Server)
|| {User, Server} <- ASL2],
{result, undefined};
set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
_Lang, XData) ->
AccountString = get_value(<<"accountjid">>, XData),
JID = jid:decode(AccountString),
LServer = JID#jid.lserver,
true = LServer == Host orelse
get_permission_level(From, Host) == global,
case JID#jid.lresource of
<<>> ->
ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver);
R ->
ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver, R)
end,
{result, undefined};
set_form(From, Host,
?NS_ADMINL(<<"change-user-password">>), _Lang, XData) ->
AccountString = get_value(<<"accountjid">>, XData),
Password = get_value(<<"password">>, XData),
JID = jid:decode(AccountString),
User = JID#jid.luser,
Server = JID#jid.lserver,
true = Server == Host orelse
get_permission_level(From, Host) == global,
true = ejabberd_auth:user_exists(User, Server),
ejabberd_auth:set_password(User, Server, Password),
{result, undefined};
set_form(From, Host,
?NS_ADMINL(<<"get-user-lastlogin">>), Lang, XData) ->
AccountString = get_value(<<"accountjid">>, XData),
JID = jid:decode(AccountString),
User = JID#jid.luser,
Server = JID#jid.lserver,
true = Server == Host orelse
get_permission_level(From, Host) == global,
FLast = case ejabberd_sm:get_user_resources(User,
Server)
of
[] ->
case get_last_info(User, Server) of
not_found -> tr(Lang, ?T("Never"));
{ok, Timestamp, _Status} ->
Shift = Timestamp,
TimeStamp = {Shift div 1000000, Shift rem 1000000, 0},
{{Year, Month, Day}, {Hour, Minute, Second}} =
calendar:now_to_local_time(TimeStamp),
(str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
[Year, Month, Day, Hour,
Minute, Second]))
end;
_ -> tr(Lang, ?T("Online"))
end,
{result,
#xdata{type = form,
fields = [?HFIELD(),
?XFIELD('jid-single', ?T("Jabber ID"),
<<"accountjid">>, AccountString),
?XFIELD('text-single', ?T("Last login"),
<<"lastlogin">>, FLast)]}};
set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang,
XData) ->
AccountString = get_value(<<"accountjid">>, XData),
JID = jid:decode(AccountString),
User = JID#jid.luser,
Server = JID#jid.lserver,
true = Server == Host orelse
get_permission_level(From, Host) == global,
Resources = ejabberd_sm:get_user_resources(User,
Server),
IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource)
|| Resource <- Resources],
IPs = [<<(misc:ip_to_list(IP))/binary, ":",
(integer_to_binary(Port))/binary>>
|| {IP, Port} <- IPs1],
Items = ejabberd_hooks:run_fold(roster_get, Server, [],
[{User, Server}]),
Rostersize = integer_to_binary(erlang:length(Items)),
{result,
#xdata{type = form,
fields = [?HFIELD(),
?XFIELD('jid-single', ?T("Jabber ID"),
<<"accountjid">>, AccountString),
?XFIELD('text-single', ?T("Roster size"),
<<"rostersize">>, Rostersize),
?XMFIELD('text-multi', ?T("IP addresses"),
<<"ipaddresses">>, IPs),
?XMFIELD('text-multi', ?T("Resources"),
<<"onlineresources">>, Resources)]}};
set_form(_From, _Host, _, _Lang, _XData) ->
{error, xmpp:err_service_unavailable()}.
-spec get_value(binary(), xdata()) -> binary().
get_value(Field, XData) ->
hd(get_values(Field, XData)).
-spec get_values(binary(), xdata()) -> [binary()].
get_values(Field, XData) ->
xmpp_util:get_xdata_values(Field, XData).
-spec search_running_node(binary()) -> false | node().
search_running_node(SNode) ->
search_running_node(SNode,
mnesia:system_info(running_db_nodes)).
-spec search_running_node(binary(), [node()]) -> false | node().
search_running_node(_, []) -> false;
search_running_node(SNode, [Node | Nodes]) ->
case atom_to_binary(Node, utf8) of
SNode -> Node;
_ -> search_running_node(SNode, Nodes)
end.
-spec stop_node(jid(), binary(), binary(), restart | stop, xdata()) -> {result, undefined}.
stop_node(From, Host, ENode, Action, XData) ->
Delay = binary_to_integer(get_value(<<"delay">>, XData)),
Subject = case get_values(<<"subject">>, XData) of
[] ->
[];
[S|_] ->
[#xdata_field{var = <<"subject">>, values = [S]}]
end,
Announcement = case get_values(<<"announcement">>, XData) of
[] ->
[];
As ->
[#xdata_field{var = <<"body">>, values = As}]
end,
case Subject ++ Announcement of
[] ->
ok;
Fields ->
Request = #adhoc_command{node = ?NS_ADMINX(<<"announce-allhosts">>),
action = complete,
xdata = #xdata{type = submit,
fields = Fields}},
To = jid:make(Host),
mod_announce:announce_commands(empty, From, To, Request)
end,
Time = timer:seconds(Delay),
Node = misc:binary_to_atom(ENode),
{ok, _} = timer:apply_after(Time, ejabberd_cluster, call, [Node, init, Action, []]),
{result, undefined}.
-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found.
get_last_info(User, Server) ->
case gen_mod:is_loaded(Server, mod_last) of
true -> mod_last:get_last_info(User, Server);
false -> not_found
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec adhoc_sm_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() |
{error, stanza_error()}.
adhoc_sm_commands(_Acc, From,
#jid{user = User, server = Server, lserver = LServer},
#adhoc_command{lang = Lang, node = <<"config">>,
action = Action, xdata = XData} = Request) ->
case acl_match_rule(LServer, From) of
deny ->
{error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
allow ->
ActionIsExecute = Action == execute orelse Action == complete,
if Action == cancel ->
xmpp_util:make_adhoc_response(
Request, #adhoc_command{status = canceled});
XData == undefined, ActionIsExecute ->
case get_sm_form(User, Server, <<"config">>, Lang) of
{result, Form} ->
xmpp_util:make_adhoc_response(
Request, #adhoc_command{status = executing,
xdata = Form});
{error, Error} ->
{error, Error}
end;
XData /= undefined, ActionIsExecute ->
set_sm_form(User, Server, <<"config">>, Request);
true ->
Txt = ?T("Unexpected action"),
{error, xmpp:err_bad_request(Txt, Lang)}
end
end;
adhoc_sm_commands(Acc, _From, _To, _Request) -> Acc.
-spec get_sm_form(binary(), binary(), binary(), binary()) -> {result, xdata()} |
{error, stanza_error()}.
get_sm_form(User, Server, <<"config">>, Lang) ->
{result,
#xdata{type = form,
title = <<(tr(Lang, ?T("Administration of ")))/binary, User/binary>>,
fields =
[?HFIELD(),
#xdata_field{
type = 'list-single',
label = tr(Lang, ?T("Action on user")),
var = <<"action">>,
values = [<<"edit">>],
options = [#xdata_option{
label = tr(Lang, ?T("Edit Properties")),
value = <<"edit">>},
#xdata_option{
label = tr(Lang, ?T("Remove User")),
value = <<"remove">>}]},
?XFIELD('text-private', ?T("Password"),
<<"password">>,
ejabberd_auth:get_password_s(User, Server))]}};
get_sm_form(_User, _Server, _Node, _Lang) ->
{error, xmpp:err_service_unavailable()}.
-spec set_sm_form(binary(), binary(), binary(), adhoc_command()) -> adhoc_command() |
{error, stanza_error()}.
set_sm_form(User, Server, <<"config">>,
#adhoc_command{lang = Lang, node = Node,
sid = SessionID, xdata = XData}) ->
Response = #adhoc_command{lang = Lang, node = Node,
sid = SessionID, status = completed},
case xmpp_util:get_xdata_values(<<"action">>, XData) of
[<<"edit">>] ->
case xmpp_util:get_xdata_values(<<"password">>, XData) of
[Password] ->
ejabberd_auth:set_password(User, Server, Password),
xmpp_util:make_adhoc_response(Response);
_ ->
Txt = ?T("No 'password' found in data form"),
{error, xmpp:err_not_acceptable(Txt, Lang)}
end;
[<<"remove">>] ->
ejabberd_auth:remove_user(User, Server),
xmpp_util:make_adhoc_response(Response);
_ ->
Txt = ?T("Incorrect value of 'action' in data form"),
{error, xmpp:err_not_acceptable(Txt, Lang)}
end;
set_sm_form(_User, _Server, _Node, _Request) ->
{error, xmpp:err_service_unavailable()}.
-spec tr(binary(), binary()) -> binary().
tr(Lang, Text) ->
translate:translate(Lang, Text).
-spec mod_opt_type(atom()) -> econf:validator().
mod_opt_type(access) ->
econf:acl().
-spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}].
mod_options(_Host) ->
[{access, configure}].
%% @format-begin
%% All ad-hoc commands implemented by mod_configure are available as API Commands:
%% - add-user -> register
%% - delete-user -> unregister
%% - end-user-session -> kick_session / kick_user
%% - change-user-password -> change_password
%% - get-user-lastlogin -> get_last
%% - user-stats -> user_sessions_info
%% - get-registered-users-list -> registered_users
%% - get-registered-users-num -> stats
%% - get-online-users-list -> connected_users
%% - get-online-users-num -> stats
%% - stopped nodes -> list_cluster_detailed
%% - DB -> mnesia_list_tables and mnesia_table_change_storage
%% - restart -> stop_kindly / restart
%% - shutdown -> stop_kindly
%% - backup -> backup
%% - restore -> restore
%% - textfile -> dump
%% - import/file -> import_file
%% - import/dir -> import_dir
%%
%% An exclusive feature available only in this module is to list items and discover them:
%% - outgoing s2s
%% - online users
%% - all users
mod_doc() ->
#{desc =>
[?T("The module provides server configuration functionalities using "
"https://xmpp.org/extensions/xep-0030.html[XEP-0030: Service Discovery] and "
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]:"),
"",
"- List and discover outgoing s2s, online client sessions and all registered accounts",
"- Most of the ad-hoc commands defined in https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]",
"- Additional custom ad-hoc commands specific to ejabberd",
"",
?T("This module requires _`mod_adhoc`_ (to execute the commands), "
"and recommends _`mod_disco`_ (to discover the commands). "),
"",
?T("Please notice that all the ad-hoc commands implemented by this module "
"have an equivalent "
"https://docs.ejabberd.im/developer/ejabberd-api/[API Command] "
"that you can execute using _`mod_adhoc_api`_ or any other API frontend.")],
opts =>
[{access,
#{value => ?T("AccessName"),
note => "added in 25.03",
desc =>
?T("This option defines which access rule will be used to "
"control who is allowed to access the features provided by this module. "
"The default value is 'configure'.")}}],
example =>
["acl:",
" admin:",
" user: sun@localhost",
"",
"access_rules:",
" configure:",
" allow: admin",
"",
"modules:",
" mod_configure:",
" access: configure"]}.