./ct_report/coverage/ejabberd_commands.COVER.html

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 177 Matched = ets:match(ejabberd_commands,
299 #ejabberd_commands{name = Name,
300 args = '$1',
301 result = '$2',
302 _ = '_'}),
303 177 case Matched of
304 [] ->
305
:-(
{error, command_unknown};
306 [[Args, Result]] ->
307 177 {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 177 case ets:lookup(ejabberd_commands, Name) of
335 [Command] ->
336 177 try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
337 177 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 177 Module = Command#ejabberd_commands.module,
348 177 Function = Command#ejabberd_commands.function,
349 177 ?LOG_DEBUG(#{what => execute_command,
350 command_module => Module,
351 command_function => Function,
352 177 command_args => Arguments}),
353 177 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 177 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).
Line Hits Source