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_cmd() :: {Access :: atom(), |
235 |
|
CommandNames :: [atom()], |
236 |
|
Arguments :: [term()] |
237 |
|
}. |
238 |
|
-type list_cmd() :: {Name::atom(), Args::[aterm()], Desc::string()}. |
239 |
|
|
240 |
|
-export_type([rterm/0, |
241 |
|
aterm/0, |
242 |
|
cmd/0, |
243 |
|
auth/0, |
244 |
|
access_cmd/0, |
245 |
|
list_cmd/0]). |
246 |
|
|
247 |
|
|
248 |
|
init() -> |
249 |
82 |
case ets:info(ejabberd_commands) of |
250 |
|
undefined -> |
251 |
82 |
ets:new(ejabberd_commands, [named_table, set, public, |
252 |
|
{keypos, #ejabberd_commands.name}]); |
253 |
|
_ -> |
254 |
:-( |
ok |
255 |
|
end. |
256 |
|
|
257 |
|
|
258 |
|
%% @doc Register ejabberd commands. If a command is already registered, a |
259 |
|
%% warning is printed and the old command is preserved. |
260 |
|
-spec register_commands([cmd()]) -> ok. |
261 |
|
register_commands(Commands) -> |
262 |
1230 |
lists:foreach( |
263 |
|
fun(Command) -> |
264 |
5904 |
Inserted = ets:insert_new(ejabberd_commands, Command), |
265 |
5904 |
?LOG_IF(warning, not Inserted, |
266 |
|
#{what => register_command_duplicate, |
267 |
|
text => <<"This command is already defined">>, |
268 |
:-( |
command => Command}) |
269 |
|
end, |
270 |
|
Commands). |
271 |
|
|
272 |
|
|
273 |
|
%% @doc Unregister ejabberd commands. |
274 |
|
-spec unregister_commands([cmd()]) -> ok. |
275 |
|
unregister_commands(Commands) -> |
276 |
984 |
lists:foreach( |
277 |
|
fun(Command) -> |
278 |
3936 |
ets:delete_object(ejabberd_commands, Command) |
279 |
|
end, |
280 |
|
Commands). |
281 |
|
|
282 |
|
|
283 |
|
%% @doc Get a list of all the available commands, arguments and description. |
284 |
|
-spec list_commands() -> [list_cmd()]. |
285 |
|
list_commands() -> |
286 |
:-( |
Commands = ets:match(ejabberd_commands, |
287 |
|
#ejabberd_commands{name = '$1', |
288 |
|
args = '$2', |
289 |
|
desc = '$3', |
290 |
|
_ = '_'}), |
291 |
:-( |
[{A, B, C} || [A, B, C] <- Commands]. |
292 |
|
|
293 |
|
|
294 |
|
%% @doc Get the format of arguments and result of a command. |
295 |
|
-spec get_command_format(Name::atom()) -> {Args::[aterm()], Result::rterm()} |
296 |
|
| {error, command_unknown}. |
297 |
|
get_command_format(Name) -> |
298 |
154 |
Matched = ets:match(ejabberd_commands, |
299 |
|
#ejabberd_commands{name = Name, |
300 |
|
args = '$1', |
301 |
|
result = '$2', |
302 |
|
_ = '_'}), |
303 |
154 |
case Matched of |
304 |
|
[] -> |
305 |
:-( |
{error, command_unknown}; |
306 |
|
[[Args, Result]] -> |
307 |
154 |
{Args, Result} |
308 |
|
end. |
309 |
|
|
310 |
|
|
311 |
|
%% @doc Get the definition record of a command. |
312 |
|
-spec get_command_definition(Name::atom()) -> cmd() | 'command_not_found'. |
313 |
|
get_command_definition(Name) -> |
314 |
:-( |
case ets:lookup(ejabberd_commands, Name) of |
315 |
:-( |
[E] -> E; |
316 |
:-( |
[] -> command_not_found |
317 |
|
end. |
318 |
|
|
319 |
|
|
320 |
|
%% @doc Execute a command. |
321 |
|
-spec execute_command(Name :: atom(), |
322 |
|
Arguments :: list() |
323 |
|
) -> Result :: term() | {error, command_unknown}. |
324 |
|
execute_command(Name, Arguments) -> |
325 |
:-( |
execute_command([], noauth, Name, Arguments). |
326 |
|
|
327 |
|
|
328 |
|
-spec execute_command(AccessCommands :: [access_cmd()], |
329 |
|
Auth :: auth(), |
330 |
|
Name :: atom(), |
331 |
|
Arguments :: [term()] |
332 |
|
) -> Result :: term() | {error, cmd_error()}. |
333 |
|
execute_command(AccessCommands, Auth, Name, Arguments) -> |
334 |
154 |
case ets:lookup(ejabberd_commands, Name) of |
335 |
|
[Command] -> |
336 |
154 |
try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of |
337 |
154 |
ok -> execute_command2(Command, Arguments) |
338 |
|
catch |
339 |
:-( |
{error, Error} -> {error, Error} |
340 |
|
end; |
341 |
:-( |
[] -> {error, command_unknown} |
342 |
|
end. |
343 |
|
|
344 |
|
|
345 |
|
%% @private |
346 |
|
execute_command2(Command, Arguments) -> |
347 |
154 |
Module = Command#ejabberd_commands.module, |
348 |
154 |
Function = Command#ejabberd_commands.function, |
349 |
154 |
?LOG_DEBUG(#{what => execute_command, |
350 |
|
command_module => Module, |
351 |
|
command_function => Function, |
352 |
154 |
command_args => Arguments}), |
353 |
154 |
apply(Module, Function, Arguments). |
354 |
|
|
355 |
|
|
356 |
|
%% @doc Get all the tags and associated commands. |
357 |
|
-spec get_tags_commands() -> [{Tag::string(), [CommandName::string()]}]. |
358 |
|
get_tags_commands() -> |
359 |
:-( |
CommandTags = ets:match(ejabberd_commands, |
360 |
|
#ejabberd_commands{ |
361 |
|
name = '$1', |
362 |
|
tags = '$2', |
363 |
|
_ = '_'}), |
364 |
:-( |
Dict = lists:foldl( |
365 |
|
fun([CommandNameAtom, CTags], D) -> |
366 |
:-( |
CommandName = atom_to_list(CommandNameAtom), |
367 |
:-( |
case CTags of |
368 |
|
[] -> |
369 |
:-( |
orddict:append("untagged", CommandName, D); |
370 |
|
_ -> |
371 |
:-( |
lists:foldl( |
372 |
|
fun(TagAtom, DD) -> |
373 |
:-( |
Tag = atom_to_list(TagAtom), |
374 |
:-( |
orddict:append(Tag, CommandName, DD) |
375 |
|
end, |
376 |
|
D, |
377 |
|
CTags) |
378 |
|
end |
379 |
|
end, |
380 |
|
orddict:new(), |
381 |
|
CommandTags), |
382 |
:-( |
orddict:to_list(Dict). |
383 |
|
|
384 |
|
|
385 |
|
%% ----------------------------- |
386 |
|
%% Access verification |
387 |
|
%% ----------------------------- |
388 |
|
|
389 |
|
|
390 |
|
%% @doc Check access is allowed to that command. |
391 |
|
%% At least one AccessCommand must be satisfied. |
392 |
|
%% May throw {error, account_unprivileged | invalid_account_data} |
393 |
|
-spec check_access_commands(AccessCommands :: [ access_cmd() ], |
394 |
|
Auth :: auth(), |
395 |
|
Method :: atom(), |
396 |
|
Command :: tuple(), |
397 |
|
Arguments :: [any()] |
398 |
|
) -> ok | none(). |
399 |
|
check_access_commands([], _Auth, _Method, _Command, _Arguments) -> |
400 |
154 |
ok; |
401 |
|
check_access_commands(AccessCommands, Auth, Method, Command, Arguments) -> |
402 |
:-( |
AccessCommandsAllowed = |
403 |
|
lists:filter( |
404 |
|
fun({Access, Commands, ArgumentRestrictions}) -> |
405 |
:-( |
case check_access(Access, Auth) of |
406 |
|
true -> |
407 |
:-( |
check_access_command(Commands, Command, ArgumentRestrictions, |
408 |
|
Method, Arguments); |
409 |
|
false -> |
410 |
:-( |
false |
411 |
|
end |
412 |
|
end, |
413 |
|
AccessCommands), |
414 |
:-( |
case AccessCommandsAllowed of |
415 |
:-( |
[] -> throw({error, account_unprivileged}); |
416 |
:-( |
L when is_list(L) -> ok |
417 |
|
end. |
418 |
|
|
419 |
|
|
420 |
|
%% @private |
421 |
|
%% May throw {error, invalid_account_data} |
422 |
|
-spec check_auth(auth()) -> {ok, jid:jid()} | no_return(). |
423 |
|
check_auth({User, Server, Password}) -> |
424 |
|
%% Check the account exists and password is valid |
425 |
:-( |
JID = jid:make(User, Server, <<>>), |
426 |
:-( |
AccountPass = ejabberd_auth:get_password_s(JID), |
427 |
:-( |
AccountPassMD5 = get_md5(AccountPass), |
428 |
:-( |
case Password of |
429 |
:-( |
AccountPass -> {ok, JID}; |
430 |
:-( |
AccountPassMD5 -> {ok, JID}; |
431 |
:-( |
_ -> throw({error, invalid_account_data}) |
432 |
|
end. |
433 |
|
|
434 |
|
|
435 |
|
-spec get_md5(iodata()) -> string(). |
436 |
|
get_md5(AccountPass) -> |
437 |
:-( |
lists:flatten([io_lib:format("~.16B", [X]) |
438 |
:-( |
|| X <- binary_to_list(crypto:hash(md5, AccountPass))]). |
439 |
|
|
440 |
|
|
441 |
|
-spec check_access(Access :: acl:rule_name(), Auth :: auth()) -> boolean(). |
442 |
|
check_access(all, _) -> |
443 |
:-( |
true; |
444 |
|
check_access(_, noauth) -> |
445 |
:-( |
false; |
446 |
|
check_access(Access, Auth) -> |
447 |
:-( |
{ok, JID} = check_auth(Auth), |
448 |
|
%% Check this user has access permission |
449 |
:-( |
{_, LServer} = jid:to_lus(JID), |
450 |
:-( |
{ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), |
451 |
:-( |
case acl:match_rule(HostType, LServer, Access, JID) of |
452 |
:-( |
allow -> true; |
453 |
:-( |
deny -> false |
454 |
|
end. |
455 |
|
|
456 |
|
|
457 |
|
-spec check_access_command(_, tuple(), _, _, _) -> boolean(). |
458 |
|
check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> |
459 |
:-( |
case Commands==all orelse lists:member(Method, Commands) of |
460 |
:-( |
true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); |
461 |
:-( |
false -> false |
462 |
|
end. |
463 |
|
|
464 |
|
|
465 |
|
-spec check_access_arguments(Command :: cmd(), |
466 |
|
Restrictions :: [any()], |
467 |
|
Args :: [any()]) -> boolean(). |
468 |
|
check_access_arguments(Command, ArgumentRestrictions, Arguments) -> |
469 |
:-( |
ArgumentsTagged = tag_arguments(Command#ejabberd_commands.args, Arguments), |
470 |
:-( |
lists:all( |
471 |
|
fun({ArgName, ArgAllowedValue}) -> |
472 |
|
%% If the call uses the argument, check the value is acceptable |
473 |
:-( |
case lists:keysearch(ArgName, 1, ArgumentsTagged) of |
474 |
:-( |
{value, {ArgName, ArgValue}} -> ArgValue == ArgAllowedValue; |
475 |
:-( |
false -> true |
476 |
|
end |
477 |
|
end, ArgumentRestrictions). |
478 |
|
|
479 |
|
|
480 |
|
-spec tag_arguments(ArgsDefs :: [{atom(), integer() | string() | {_, _}}], |
481 |
|
Args :: [any()] ) -> [{_, _}]. |
482 |
|
tag_arguments(ArgsDefs, Args) -> |
483 |
:-( |
lists:zipwith( |
484 |
|
fun({ArgName, _ArgType}, ArgValue) -> |
485 |
:-( |
{ArgName, ArgValue} |
486 |
|
end, |
487 |
|
ArgsDefs, |
488 |
|
Args). |