1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_commands.erl |
3 |
|
%%% Author : Badlop <badlop@process-one.net> |
4 |
|
%%% Purpose : Management of ejabberd commands |
5 |
|
%%% Created : 20 May 2008 by Badlop <badlop@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
%%% @headerfile "ejabberd_commands.hrl" |
27 |
|
|
28 |
|
%%% @doc Management of ejabberd commands. |
29 |
|
%%% |
30 |
|
%%% An ejabberd command is an abstract function identified by a name, |
31 |
|
%%% with a defined number and type of calling arguments and type of |
32 |
|
%%% result, that can be defined in any Erlang module and executed |
33 |
|
%%% using any valid frontend. |
34 |
|
%%% |
35 |
|
%%% |
36 |
|
%%% == Define a new ejabberd command == |
37 |
|
%%% |
38 |
|
%%% ejabberd commands can be defined and registered in |
39 |
|
%%% any Erlang module. |
40 |
|
%%% |
41 |
|
%%% Some commands are procedures; and their purpose is to perform an |
42 |
|
%%% action in the server, so the command result is only some result |
43 |
|
%%% code or result tuple. Other commands are inspectors, and their |
44 |
|
%%% purpose is to gather some information about the server and return |
45 |
|
%%% a detailed response: it can be integer, string, atom, tuple, list |
46 |
|
%%% or a mix of those ones. |
47 |
|
%%% |
48 |
|
%%% The arguments and result of an ejabberd command are strictly |
49 |
|
%%% defined. The number and format of the arguments provided when |
50 |
|
%%% calling an ejabberd command must match the definition of that |
51 |
|
%%% command. The format of the result provided by an ejabberd command |
52 |
|
%%% must be exactly its definition. For example, if a command is said |
53 |
|
%%% to return an integer, it must always return an integer (except in |
54 |
|
%%% case of a crash). |
55 |
|
%%% |
56 |
|
%%% If you are developing an Erlang module that will run inside |
57 |
|
%%% ejabberd and you want to provide a new ejabberd command to |
58 |
|
%%% administer some task related to your module, you only need to: |
59 |
|
%%% implement a function, define the command, and register it. |
60 |
|
%%% |
61 |
|
%%% |
62 |
|
%%% === Define a new ejabberd command === |
63 |
|
%%% |
64 |
|
%%% An ejabberd command is defined using the Erlang record |
65 |
|
%%% 'ejabberd_commands'. This record has several elements that you |
66 |
|
%%% must define. Note that 'tags', 'desc' and 'longdesc' are optional. |
67 |
|
%%% |
68 |
|
%%% For example let's define an ejabberd command 'pow' that gets the |
69 |
|
%%% integers 'base' and 'exponent'. Its result will be an integer |
70 |
|
%%% 'power': |
71 |
|
%%% |
72 |
|
%%% <pre>#ejabberd_commands{name = pow, tags = [test], |
73 |
|
%%% desc = "Return the power of base for exponent", |
74 |
|
%%% longdesc = "This is an example command. The formula is:\n" |
75 |
|
%%% " power = base ^ exponent", |
76 |
|
%%% module = ?MODULE, function = pow, |
77 |
|
%%% args = [{base, integer}, {exponent, integer}], |
78 |
|
%%% result = {power, integer}}</pre> |
79 |
|
%%% |
80 |
|
%%% |
81 |
|
%%% === Implement the function associated to the command === |
82 |
|
%%% |
83 |
|
%%% Now implement a function in your module that matches the arguments |
84 |
|
%%% and result of the ejabberd command. |
85 |
|
%%% |
86 |
|
%%% For example the function calc_power gets two integers Base and |
87 |
|
%%% Exponent. It calculates the power and rounds to an integer: |
88 |
|
%%% |
89 |
|
%%% <pre>calc_power(Base, Exponent) -> |
90 |
|
%%% PowFloat = math:pow(Base, Exponent), |
91 |
|
%%% round(PowFloat).</pre> |
92 |
|
%%% |
93 |
|
%%% Since this function will be called by ejabberd_commands, it must be exported. |
94 |
|
%%% Add to your module: |
95 |
|
%%% <pre>-export([calc_power/2]).</pre> |
96 |
|
%%% |
97 |
|
%%% Only some types of result formats are allowed. |
98 |
|
%%% If the format is defined as 'rescode', then your function must return: |
99 |
|
%%% ok | true | atom() |
100 |
|
%%% where the atoms ok and true as considered positive answers, |
101 |
|
%%% and any other response atom is considered negative. |
102 |
|
%%% |
103 |
|
%%% If the format is defined as 'restuple', then the command must return: |
104 |
|
%%% {rescode(), string()} |
105 |
|
%%% |
106 |
|
%%% If the format is defined as '{list, something()}', then the command |
107 |
|
%%% must return a list of something(). |
108 |
|
%%% |
109 |
|
%%% |
110 |
|
%%% === Register the command === |
111 |
|
%%% |
112 |
|
%%% Define this function and put inside the #ejabberd_command you |
113 |
|
%%% defined in the beginning: |
114 |
|
%%% |
115 |
|
%%% <pre>commands() -> |
116 |
|
%%% [ |
117 |
|
%%% |
118 |
|
%%% ].</pre> |
119 |
|
%%% |
120 |
|
%%% You need to include this header file in order to use the record: |
121 |
|
%%% |
122 |
|
%%% <pre>-include("ejabberd_commands.hrl").</pre> |
123 |
|
%%% |
124 |
|
%%% When your module is initialized or started, register your commands: |
125 |
|
%%% |
126 |
|
%%% <pre>ejabberd_commands:register_commands(commands()), </pre> |
127 |
|
%%% |
128 |
|
%%% And when your module is stopped, unregister your commands: |
129 |
|
%%% |
130 |
|
%%% <pre>ejabberd_commands:unregister_commands(commands()), </pre> |
131 |
|
%%% |
132 |
|
%%% That's all! Now when your module is started, the command will be |
133 |
|
%%% registered and any frontend can access it. For example: |
134 |
|
%%% |
135 |
|
%%% <pre>$ mongooseimctl help pow |
136 |
|
%%% |
137 |
|
%%% Command Name: pow |
138 |
|
%%% |
139 |
|
%%% Arguments: base::integer |
140 |
|
%%% exponent::integer |
141 |
|
%%% |
142 |
|
%%% Returns: power::integer |
143 |
|
%%% |
144 |
|
%%% Tags: test |
145 |
|
%%% |
146 |
|
%%% Description: Return the power of base for exponent |
147 |
|
%%% |
148 |
|
%%% This is an example command. The formula is: |
149 |
|
%%% power = base ^ exponent |
150 |
|
%%% |
151 |
|
%%% $ mongooseimctl pow 3 4 |
152 |
|
%%% 81 |
153 |
|
%%% </pre> |
154 |
|
%%% |
155 |
|
%%% |
156 |
|
%%% == Execute an ejabberd command == |
157 |
|
%%% |
158 |
|
%%% ejabberd commands are mean to be executed using any valid |
159 |
|
%%% frontend. An ejabberd commands is implemented in a regular Erlang |
160 |
|
%%% function, so it is also possible to execute this function in any |
161 |
|
%%% Erlang module, without dealing with the associated ejabberd |
162 |
|
%%% commands. |
163 |
|
%%% |
164 |
|
%%% |
165 |
|
%%% == Frontend to ejabberd commands == |
166 |
|
%%% |
167 |
|
%%% Currently there is one frontend to ejabberd commands: the shell |
168 |
|
%%% script - mongooseimctl |
169 |
|
%%% |
170 |
|
%%% === mongooseimctl as a frontend to ejabberd commands === |
171 |
|
%%% |
172 |
|
%%% It is possible to use mongooseimctl to get documentation of any |
173 |
|
%%% command. But mongooseimctl does not support all the argument types |
174 |
|
%%% allowed in ejabberd commands, so there are some ejabberd commands |
175 |
|
%%% that cannot be executed using mongooseimctl. |
176 |
|
%%% |
177 |
|
%%% Also note that the mongooseimctl shell administration script also |
178 |
|
%%% manages mongooseimctl commands, which are unrelated to ejabberd |
179 |
|
%%% commands and can only be executed using mongooseimctl. |
180 |
|
%%% |
181 |
|
%%% TODO: consider this feature: |
182 |
|
%%% All commands are catched. If an error happens, return the restuple: |
183 |
|
%%% {error, flattened error string} |
184 |
|
%%% This means that ecomm call APIs ejabberd_ctl need to allows this. |
185 |
|
|
186 |
|
|
187 |
|
-module(ejabberd_commands). |
188 |
|
-author('badlop@process-one.net'). |
189 |
|
|
190 |
|
-export([init/0, |
191 |
|
list_commands/0, |
192 |
|
get_command_format/1, |
193 |
|
get_command_definition/1, |
194 |
|
get_tags_commands/0, |
195 |
|
register_commands/1, |
196 |
|
unregister_commands/1, |
197 |
|
execute_command/2, |
198 |
|
execute_command/4 |
199 |
|
]). |
200 |
|
|
201 |
|
-ignore_xref([execute_command/2]). |
202 |
|
|
203 |
|
-include("ejabberd_commands.hrl"). |
204 |
|
-include("mongoose.hrl"). |
205 |
|
|
206 |
|
%% Allowed types for arguments are integer, string, tuple and list. |
207 |
|
-type atype() :: integer | string | binary | {tuple, [aterm()]} | {list, aterm()}. |
208 |
|
|
209 |
|
%% A rtype is either an atom or a tuple with two elements. |
210 |
|
-type rtype() :: integer | string | atom | binary | {tuple, [rterm()]} |
211 |
|
| {list, rterm()} | rescode | restuple. |
212 |
|
|
213 |
|
%% An argument term is a tuple with the term name and the term type. |
214 |
|
-type aterm() :: {Name::atom(), Type::atype()}. |
215 |
|
|
216 |
|
%% A result term is a tuple with the term name and the term type. |
217 |
|
-type rterm() :: {Name::atom(), Type::rtype()}. |
218 |
|
|
219 |
|
-type cmd() :: #ejabberd_commands{ |
220 |
|
name :: atom(), |
221 |
|
tags :: [atom()], |
222 |
|
desc :: string(), |
223 |
|
longdesc :: string(), |
224 |
|
module :: module(), |
225 |
|
function :: atom(), |
226 |
|
args :: [ejabberd_commands:aterm()], |
227 |
|
result :: ejabberd_commands:rterm() |
228 |
|
}. |
229 |
|
|
230 |
|
-type auth() :: {User :: binary(), Server :: binary(), Password :: binary()} | noauth. |
231 |
|
|
232 |
|
-type cmd_error() :: command_unknown | account_unprivileged |
233 |
|
| invalid_account_data | no_auth_provided. |
234 |
|
-type access_commands() :: #{acl:rule_name() => command_rules()}. |
235 |
|
-type command_rules() :: #{commands := all | [atom()], |
236 |
|
argument_restrictions := argument_restrictions()}. |
237 |
|
|
238 |
|
%% Currently only string arguments can have restrictions |
239 |
|
-type argument_restrictions() :: #{ArgName :: atom() => Value :: string()}. |
240 |
|
|
241 |
|
-type list_cmd() :: {Name::atom(), Args::[aterm()], Desc::string()}. |
242 |
|
|
243 |
|
-export_type([rterm/0, |
244 |
|
aterm/0, |
245 |
|
cmd/0, |
246 |
|
auth/0, |
247 |
|
access_commands/0, |
248 |
|
list_cmd/0]). |
249 |
|
|
250 |
|
init() -> |
251 |
83 |
case ets:info(ejabberd_commands) of |
252 |
|
undefined -> |
253 |
83 |
ets:new(ejabberd_commands, [named_table, set, public, |
254 |
|
{keypos, #ejabberd_commands.name}]); |
255 |
|
_ -> |
256 |
:-( |
ok |
257 |
|
end. |
258 |
|
|
259 |
|
%% @doc Register ejabberd commands. If a command is already registered, a |
260 |
|
%% warning is printed and the old command is preserved. |
261 |
|
-spec register_commands([cmd()]) -> ok. |
262 |
|
register_commands(Commands) -> |
263 |
1245 |
lists:foreach( |
264 |
|
fun(Command) -> |
265 |
5976 |
Inserted = ets:insert_new(ejabberd_commands, Command), |
266 |
5976 |
?LOG_IF(warning, not Inserted, |
267 |
|
#{what => register_command_duplicate, |
268 |
|
text => <<"This command is already defined">>, |
269 |
:-( |
command => Command}) |
270 |
|
end, |
271 |
|
Commands). |
272 |
|
|
273 |
|
%% @doc Unregister ejabberd commands. |
274 |
|
-spec unregister_commands([cmd()]) -> ok. |
275 |
|
unregister_commands(Commands) -> |
276 |
996 |
lists:foreach( |
277 |
|
fun(Command) -> |
278 |
3984 |
ets:delete_object(ejabberd_commands, Command) |
279 |
|
end, |
280 |
|
Commands). |
281 |
|
|
282 |
|
%% @doc Get a list of all the available commands, arguments and description. |
283 |
|
-spec list_commands() -> [list_cmd()]. |
284 |
|
list_commands() -> |
285 |
:-( |
Commands = ets:match(ejabberd_commands, |
286 |
|
#ejabberd_commands{name = '$1', |
287 |
|
args = '$2', |
288 |
|
desc = '$3', |
289 |
|
_ = '_'}), |
290 |
:-( |
[{A, B, C} || [A, B, C] <- Commands]. |
291 |
|
|
292 |
|
%% @doc Get the format of arguments and result of a command. |
293 |
|
-spec get_command_format(Name::atom()) -> {Args::[aterm()], Result::rterm()} |
294 |
|
| {error, command_unknown}. |
295 |
|
get_command_format(Name) -> |
296 |
226 |
Matched = ets:match(ejabberd_commands, |
297 |
|
#ejabberd_commands{name = Name, |
298 |
|
args = '$1', |
299 |
|
result = '$2', |
300 |
|
_ = '_'}), |
301 |
226 |
case Matched of |
302 |
|
[] -> |
303 |
:-( |
{error, command_unknown}; |
304 |
|
[[Args, Result]] -> |
305 |
226 |
{Args, Result} |
306 |
|
end. |
307 |
|
|
308 |
|
%% @doc Get the definition record of a command. |
309 |
|
-spec get_command_definition(Name::atom()) -> cmd() | 'command_not_found'. |
310 |
|
get_command_definition(Name) -> |
311 |
:-( |
case ets:lookup(ejabberd_commands, Name) of |
312 |
:-( |
[E] -> E; |
313 |
:-( |
[] -> command_not_found |
314 |
|
end. |
315 |
|
|
316 |
|
%% @doc Execute a command. |
317 |
|
-spec execute_command(Name :: atom(), |
318 |
|
Arguments :: list() |
319 |
|
) -> Result :: term() | {error, command_unknown}. |
320 |
|
execute_command(Name, Arguments) -> |
321 |
:-( |
execute_command(#{}, noauth, Name, Arguments). |
322 |
|
|
323 |
|
-spec execute_command(AccessCommands :: access_commands(), |
324 |
|
Auth :: auth(), |
325 |
|
Name :: atom(), |
326 |
|
Arguments :: [term()] |
327 |
|
) -> Result :: term() | {error, cmd_error()}. |
328 |
|
execute_command(AccessCommands, Auth, Name, Arguments) -> |
329 |
226 |
case ets:lookup(ejabberd_commands, Name) of |
330 |
|
[Command] -> |
331 |
226 |
try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of |
332 |
226 |
ok -> execute_command2(Command, Arguments) |
333 |
|
catch |
334 |
:-( |
{error, Error} -> {error, Error} |
335 |
|
end; |
336 |
:-( |
[] -> {error, command_unknown} |
337 |
|
end. |
338 |
|
|
339 |
|
%% @private |
340 |
|
execute_command2(Command, Arguments) -> |
341 |
226 |
Module = Command#ejabberd_commands.module, |
342 |
226 |
Function = Command#ejabberd_commands.function, |
343 |
226 |
?LOG_DEBUG(#{what => execute_command, |
344 |
|
command_module => Module, |
345 |
|
command_function => Function, |
346 |
226 |
command_args => Arguments}), |
347 |
226 |
apply(Module, Function, Arguments). |
348 |
|
|
349 |
|
%% @doc Get all the tags and associated commands. |
350 |
|
-spec get_tags_commands() -> [{Tag::string(), [CommandName::string()]}]. |
351 |
|
get_tags_commands() -> |
352 |
:-( |
CommandTags = ets:match(ejabberd_commands, |
353 |
|
#ejabberd_commands{ |
354 |
|
name = '$1', |
355 |
|
tags = '$2', |
356 |
|
_ = '_'}), |
357 |
:-( |
Dict = lists:foldl( |
358 |
|
fun([CommandNameAtom, CTags], D) -> |
359 |
:-( |
CommandName = atom_to_list(CommandNameAtom), |
360 |
:-( |
case CTags of |
361 |
|
[] -> |
362 |
:-( |
orddict:append("untagged", CommandName, D); |
363 |
|
_ -> |
364 |
:-( |
lists:foldl( |
365 |
|
fun(TagAtom, DD) -> |
366 |
:-( |
Tag = atom_to_list(TagAtom), |
367 |
:-( |
orddict:append(Tag, CommandName, DD) |
368 |
|
end, |
369 |
|
D, |
370 |
|
CTags) |
371 |
|
end |
372 |
|
end, |
373 |
|
orddict:new(), |
374 |
|
CommandTags), |
375 |
:-( |
orddict:to_list(Dict). |
376 |
|
|
377 |
|
%% ----------------------------- |
378 |
|
%% Access verification |
379 |
|
%% ----------------------------- |
380 |
|
|
381 |
|
%% @doc Check access is allowed to that command. |
382 |
|
%% At least one AccessCommand must be satisfied. |
383 |
|
%% May throw {error, account_unprivileged | invalid_account_data} |
384 |
|
-spec check_access_commands(AccessCommands :: access_commands(), |
385 |
|
Auth :: auth(), |
386 |
|
Method :: atom(), |
387 |
|
Command :: tuple(), |
388 |
|
Arguments :: [any()] |
389 |
|
) -> ok | none(). |
390 |
|
check_access_commands(AccessCommands, _Auth, _Method, _Command, _Arguments) |
391 |
226 |
when AccessCommands =:= #{} -> ok; |
392 |
|
check_access_commands(AccessCommands, Auth, Method, Command, Arguments) -> |
393 |
:-( |
AccessCommandsAllowed = |
394 |
|
maps:filter( |
395 |
|
fun(Access, CommandSpec) -> |
396 |
:-( |
case check_access(Access, Auth) of |
397 |
|
true -> |
398 |
:-( |
check_access_command(Command, CommandSpec, Method, Arguments); |
399 |
|
false -> |
400 |
:-( |
false |
401 |
|
end |
402 |
|
end, |
403 |
|
AccessCommands), |
404 |
:-( |
case AccessCommandsAllowed =:= #{} of |
405 |
:-( |
true -> throw({error, account_unprivileged}); |
406 |
:-( |
false -> ok |
407 |
|
end. |
408 |
|
|
409 |
|
%% @private |
410 |
|
%% May throw {error, invalid_account_data} |
411 |
|
-spec check_auth(auth()) -> {ok, jid:jid()} | no_return(). |
412 |
|
check_auth({User, Server, Password}) -> |
413 |
|
%% Check the account exists and password is valid |
414 |
:-( |
JID = jid:make(User, Server, <<>>), |
415 |
:-( |
AccountPass = ejabberd_auth:get_password_s(JID), |
416 |
:-( |
AccountPassMD5 = get_md5(AccountPass), |
417 |
:-( |
case Password of |
418 |
:-( |
AccountPass -> {ok, JID}; |
419 |
:-( |
AccountPassMD5 -> {ok, JID}; |
420 |
:-( |
_ -> throw({error, invalid_account_data}) |
421 |
|
end. |
422 |
|
|
423 |
|
-spec get_md5(iodata()) -> string(). |
424 |
|
get_md5(AccountPass) -> |
425 |
:-( |
lists:flatten([io_lib:format("~.16B", [X]) |
426 |
:-( |
|| X <- binary_to_list(crypto:hash(md5, AccountPass))]). |
427 |
|
|
428 |
|
-spec check_access(Access :: acl:rule_name(), Auth :: auth()) -> boolean(). |
429 |
|
check_access(all, _) -> |
430 |
:-( |
true; |
431 |
|
check_access(_, noauth) -> |
432 |
:-( |
false; |
433 |
|
check_access(Access, Auth) -> |
434 |
:-( |
{ok, JID} = check_auth(Auth), |
435 |
|
%% Check this user has access permission |
436 |
:-( |
{_, LServer} = jid:to_lus(JID), |
437 |
:-( |
{ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), |
438 |
:-( |
case acl:match_rule(HostType, LServer, Access, JID) of |
439 |
:-( |
allow -> true; |
440 |
:-( |
deny -> false |
441 |
|
end. |
442 |
|
|
443 |
|
-spec check_access_command(cmd(), command_rules(), atom(), [any()]) -> boolean(). |
444 |
|
check_access_command(Command, CommandRules, Method, Arguments) -> |
445 |
:-( |
#{commands := Commands, argument_restrictions := ArgumentRestrictions} = CommandRules, |
446 |
:-( |
case Commands == all orelse lists:member(Method, Commands) of |
447 |
:-( |
true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); |
448 |
:-( |
false -> false |
449 |
|
end. |
450 |
|
|
451 |
|
-spec check_access_arguments(Command :: cmd(), |
452 |
|
Restrictions :: [any()], |
453 |
|
Args :: [any()]) -> boolean(). |
454 |
|
check_access_arguments(Command, ArgumentRestrictions, Arguments) -> |
455 |
:-( |
ArgumentsTagged = tag_arguments(Command#ejabberd_commands.args, Arguments), |
456 |
:-( |
lists:all( |
457 |
|
fun({ArgName, ArgValue}) -> |
458 |
:-( |
case ArgumentRestrictions of |
459 |
|
%% If there is a restriction, check the value is acceptable |
460 |
:-( |
#{ArgName := ArgAllowedValue} -> ArgValue =:= ArgAllowedValue; |
461 |
:-( |
#{} -> true |
462 |
|
end |
463 |
|
end, ArgumentsTagged). |
464 |
|
|
465 |
|
-spec tag_arguments(ArgsDefs :: [{atom(), integer() | string() | {_, _}}], |
466 |
|
Args :: [any()] ) -> [{_, _}]. |
467 |
|
tag_arguments(ArgsDefs, Args) -> |
468 |
:-( |
lists:zipwith( |
469 |
|
fun({ArgName, _ArgType}, ArgValue) -> |
470 |
:-( |
{ArgName, ArgValue} |
471 |
|
end, |
472 |
|
ArgsDefs, |
473 |
|
Args). |