diff options
author | Mickaël Rémond <mickael.remond@process-one.net> | 2016-04-01 14:24:08 +0200 |
---|---|---|
committer | Mickaël Rémond <mickael.remond@process-one.net> | 2016-04-01 14:24:08 +0200 |
commit | ca9ac019eb4a3abaade0c4c9eb31e0fe83ed875d (patch) | |
tree | 122ea1e7de3bc03f1074ec4a1feba3ddfa92c719 /src | |
parent | Test / Document ejabberd_commands checks (diff) | |
parent | Apply fixes and remove tests for missing methods (diff) |
Merge pull request #1046 from processone/commands-update
Add support for versioning in ejabberd commands
Diffstat (limited to 'src')
-rw-r--r-- | src/ejabberd_commands.erl | 293 | ||||
-rw-r--r-- | src/ejabberd_ctl.erl | 141 | ||||
-rw-r--r-- | src/mod_admin_extra.erl | 48 | ||||
-rw-r--r-- | src/mod_http_api.erl | 169 |
4 files changed, 428 insertions, 223 deletions
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 57285d6c..dd14748d 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -90,7 +90,8 @@ %%% PowFloat = math:pow(Base, Exponent), %%% round(PowFloat).</pre> %%% -%%% Since this function will be called by ejabberd_commands, it must be exported. +%%% Since this function will be called by ejabberd_commands, it must +%%% be exported. %%% Add to your module: %%% <pre>-export([calc_power/2]).</pre> %%% @@ -201,25 +202,34 @@ %%% TODO: consider this feature: %%% All commands are catched. If an error happens, return the restuple: %%% {error, flattened error string} -%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. -%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. +%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) +%%% need to allows this. And ejabberd_xmlrpc must be prepared to +%%% handle such an unexpected response. -module(ejabberd_commands). -author('badlop@process-one.net'). +-define(DEFAULT_VERSION, 1000000). + -export([init/0, list_commands/0, + list_commands/1, get_command_format/1, - get_command_format/2, + get_command_format/2, + get_command_format/3, get_command_policy/1, get_command_definition/1, + get_command_definition/2, get_tags_commands/0, + get_tags_commands/1, get_commands/0, register_commands/1, unregister_commands/1, execute_command/2, - execute_command/4, + execute_command/3, + execute_command/4, + execute_command/5, opt_type/1, get_commands_spec/0 ]). @@ -227,6 +237,7 @@ -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -define(POLICY_ACCESS, '$policy'). @@ -261,23 +272,26 @@ get_commands_spec() -> args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], result_example = ok}]. init() -> - ets:new(ejabberd_commands, [named_table, set, public, - {keypos, #ejabberd_commands.name}]), + mnesia:delete_table(ejabberd_commands), + mnesia:create_table(ejabberd_commands, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), + mnesia:add_table_copy(ejabberd_commands, node(), ram_copies), register_commands(get_commands_spec()). -spec register_commands([ejabberd_commands()]) -> ok. %% @doc Register ejabberd commands. -%% If a command is already registered, a warning is printed and the old command is preserved. +%% If a command is already registered, a warning is printed and the +%% old command is preserved. register_commands(Commands) -> lists:foreach( fun(Command) -> - case ets:insert_new(ejabberd_commands, Command) of - true -> - ok; - false -> - ?DEBUG("This command is already defined:~n~p", [Command]) - end + % XXX check if command exists + mnesia:dirty_write(Command) + % ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands). @@ -287,7 +301,7 @@ register_commands(Commands) -> unregister_commands(Commands) -> lists:foreach( fun(Command) -> - ets:delete_object(ejabberd_commands, Command) + mnesia:dirty_delete_object(Command) end, Commands). @@ -295,47 +309,59 @@ unregister_commands(Commands) -> %% @doc Get a list of all the available commands, arguments and description. list_commands() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - _ = '_'}), - [{A, B, C} || [A, B, C] <- Commands]. - --spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}]. - -%% @doc Get a list of all the available commands, arguments, description, and -%% policy. -list_commands_policy() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - policy = '$4', - _ = '_'}), - [{A, B, C, D} || [A, B, C, D] <- Commands]. - --spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}. + list_commands(?DEFAULT_VERSION). + +-spec list_commands(integer()) -> [{atom(), [aterm()], string()}]. + +%% @doc Get a list of all the available commands, arguments and +%% description in a given API verion. +list_commands(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc} || #ejabberd_commands{name = Name, + args = Args, + desc = Desc} <- Commands]. + + +-spec list_commands_policy(integer()) -> + [{atom(), [aterm()], string(), atom()}]. + +%% @doc Get a list of all the available commands, arguments, +%% description, and policy in a given API version. +list_commands_policy(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc, Policy} || + #ejabberd_commands{name = Name, + args = Args, + desc = Desc, + policy = Policy} <- Commands]. + +-spec get_command_format(atom()) -> {[aterm()], rterm()}. %% @doc Get the format of arguments and result of a command. get_command_format(Name) -> - get_command_format(Name, noauth). - -get_command_format(Name, Auth) -> + get_command_format(Name, noauth, ?DEFAULT_VERSION). +get_command_format(Name, Version) when is_integer(Version) -> + get_command_format(Name, noauth, Version); +get_command_format(Name, Auth) -> + get_command_format(Name, Auth, ?DEFAULT_VERSION). + +-spec get_command_format(atom(), + {binary(), binary(), binary(), boolean()} | + noauth | admin, + integer()) -> + {[aterm()], rterm()}. + +get_command_format(Name, Auth, Version) -> Admin = is_admin(Name, Auth), - Matched = ets:match(ejabberd_commands, - #ejabberd_commands{name = Name, - args = '$1', - result = '$2', - policy = '$3', - _ = '_'}), - case Matched of - [] -> - {error, command_unknown}; - [[Args, Result, user]] when Admin; - Auth == noauth -> + #ejabberd_commands{args = Args, + result = Result, + policy = Policy} = + get_command_definition(Name, Version), + case Policy of + user when Admin; + Auth == noauth -> {[{user, binary}, {server, binary} | Args], Result}; - [[Args, Result, _]] -> + _ -> {Args, Result} end. @@ -350,50 +376,127 @@ get_command_policy(Name) -> {error, command_not_found} end. --spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found. +-spec get_command_definition(atom()) -> ejabberd_commands(). %% @doc Get the definition record of a command. get_command_definition(Name) -> - case ets:lookup(ejabberd_commands, Name) of - [E] -> E; - [] -> command_not_found + get_command_definition(Name, ?DEFAULT_VERSION). + +-spec get_command_definition(atom(), integer()) -> ejabberd_commands(). + +%% @doc Get the definition record of a command in a given API version. +get_command_definition(Name, Version) -> + case lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = N, version = V} = C) + when N == Name, V =< Version -> + {V, C} + end)))) of + [{_, Command} | _ ] -> Command; + _E -> throw(unknown_command) end. -%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} +-spec get_commands_definition(integer()) -> [ejabberd_commands()]. + +% @doc Returns all commands for a given API version +get_commands_definition(Version) -> + L = lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = Name, version = V} = C) + when V =< Version -> + {Name, V, C} + end)))), + F = fun({_Name, _V, Command}, []) -> + [Command]; + ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + Acc; + ({_Name, _V, Command}, Acc) -> [Command | Acc] + end, + lists:foldl(F, [], L). + +%% @spec (Name::atom(), Arguments) -> ResultTerm +%% where +%% Arguments = [any()] %% @doc Execute a command. +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | +%% no_auth_provided execute_command(Name, Arguments) -> - execute_command([], noauth, Name, Arguments). + execute_command(Name, Arguments, ?DEFAULT_VERSION). + +-spec execute_command(atom(), + [any()], + integer() | + {binary(), binary(), binary(), boolean()} | + noauth | admin + ) -> any(). + +%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm +%% where +%% Auth = {User::string(), Server::string(), Password::string(), +%% Admin::boolean()} +%% | noauth +%% | admin +%% Arguments = [any()] +%% +%% @doc Execute a command in a given API version +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | +%% no_auth_provided +execute_command(Name, Arguments, Version) when is_integer(Version) -> + execute_command([], noauth, Name, Arguments, Version); +execute_command(Name, Arguments, Auth) -> + execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION). + +%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> +%% ResultTerm | {error, Error} +%% where +%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined +%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} +%% | noauth +%% | admin +%% Arguments = [any()] +%% +%% @doc Execute a command +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +execute_command(AccessCommands, Auth, Name, Arguments) -> + execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION). -spec execute_command([{atom(), [atom()], [any()]}] | undefined, {binary(), binary(), binary(), boolean()} | noauth | admin, atom(), - [any()] + [any()], + integer() ) -> any(). -%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error} +%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm %% where %% AccessCommands = [{Access, CommandNames, Arguments}] | undefined %% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} %% | noauth %% | admin -%% Method = atom() %% Arguments = [any()] -%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided -execute_command(AccessCommands1, Auth1, Name, Arguments) -> +%% +%% @doc Execute a command in a given API version +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> Auth = case is_admin(Name, Auth1) of true -> admin; false -> Auth1 end, - case ets:lookup(ejabberd_commands, Name) of - [Command] -> - AccessCommands = get_access_commands(AccessCommands1), - try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of - ok -> execute_command2(Auth, Command, Arguments) - catch - {error, Error} -> {error, Error} - end; - [] -> {error, command_unknown} + Command = get_command_definition(Name, Version), + AccessCommands = get_access_commands(AccessCommands1, Version), + case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of + ok -> execute_command2(Auth, Command, Arguments) end. execute_command2( @@ -419,26 +522,25 @@ execute_command2(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), - try apply(Module, Function, Arguments) of - Response -> - Response - catch - Problem -> - {error, Problem} - end. + apply(Module, Function, Arguments). -spec get_tags_commands() -> [{string(), [string()]}]. %% @spec () -> [{Tag::string(), [CommandName::string()]}] %% @doc Get all the tags and associated commands. get_tags_commands() -> - CommandTags = ets:match(ejabberd_commands, - #ejabberd_commands{ - name = '$1', - tags = '$2', - _ = '_'}), + get_tags_commands(?DEFAULT_VERSION). + +-spec get_tags_commands(integer()) -> [{string(), [string()]}]. + +%% @spec (integer) -> [{Tag::string(), [CommandName::string()]}] +%% @doc Get all the tags and associated commands in a given API version +get_tags_commands(Version) -> + CommandTags = [{Name, Tags} || + #ejabberd_commands{name = Name, tags = Tags} + <- get_commands_definition(Version)], Dict = lists:foldl( - fun([CommandNameAtom, CTags], D) -> + fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), case CTags of [] -> @@ -457,7 +559,6 @@ get_tags_commands() -> CommandTags), orddict:to_list(Dict). - %% ----------------------------- %% Access verification %% ----------------------------- @@ -491,7 +592,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> fun({Access, Commands, ArgumentRestrictions}) -> case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -500,7 +602,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> ArgumentRestrictions = [], case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -563,9 +666,11 @@ check_access2(Access, User, Server) -> deny -> false end. -check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> +check_access_command(Commands, Command, ArgumentRestrictions, + Method, Arguments) -> case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); + true -> check_access_arguments(Command, ArgumentRestrictions, + Arguments); false -> false end. @@ -589,18 +694,20 @@ tag_arguments(ArgsDefs, Args) -> Args). -get_access_commands(undefined) -> - Cmds = get_commands(), +get_access_commands(undefined, Version) -> + Cmds = get_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; -get_access_commands(AccessCommands) -> +get_access_commands(AccessCommands, _Version) -> AccessCommands. get_commands() -> + get_commands(?DEFAULT_VERSION). +get_commands(Version) -> Opts = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, []), - CommandsList = list_commands_policy(), + CommandsList = list_commands_policy(Version), OpenCmds = [N || {N, _, _, open} <- CommandsList], RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList], AdminCmds = [N || {N, _, _, admin} <- CommandsList], diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index bf4e4675..edec5a07 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -48,7 +48,7 @@ -behaviour(ejabberd_config). -author('alexey@process-one.net'). --export([start/0, init/0, process/1, process2/2, +-export([start/0, init/0, process/1, register_commands/3, unregister_commands/3, opt_type/1]). @@ -57,6 +57,8 @@ -include("ejabberd.hrl"). -include("logger.hrl"). +-define(DEFAULT_VERSION, 1000000). + %%----------------------------- %% Module @@ -69,7 +71,7 @@ start() -> [SNode3 | Args3] -> [SNode3, 60000, Args3]; _ -> - print_usage(), + print_usage(?DEFAULT_VERSION), halt(?STATUS_USAGE) end, SNode1 = case string:tokens(SNode, "@") of @@ -93,6 +95,9 @@ start() -> [Node, Reason]), %% TODO: show minimal start help ?STATUS_BADRPC; + {invalid_version, V} -> + print("Invalid API version number: ~p~n", [V]), + ?STATUS_ERROR; S -> S end, @@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) -> %% Process %%----------------------------- + -spec process([string()]) -> non_neg_integer(). +process(Args) -> + process(Args, ?DEFAULT_VERSION). + + +-spec process([string()], non_neg_integer()) -> non_neg_integer(). %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. -process(["status"]) -> +process(["status"], _Version) -> {InternalStatus, ProvidedStatus} = init:get_status(), print("The node ~p is ~p with status: ~p~n", [node(), InternalStatus, ProvidedStatus]), @@ -146,24 +157,24 @@ process(["status"]) -> ?STATUS_SUCCESS end; -process(["stop"]) -> +process(["stop"], _Version) -> %%ejabberd_cover:stop(), init:stop(), ?STATUS_SUCCESS; -process(["restart"]) -> +process(["restart"], _Version) -> init:restart(), ?STATUS_SUCCESS; -process(["mnesia"]) -> +process(["mnesia"], _Version) -> print("~p~n", [mnesia:system_info(all)]), ?STATUS_SUCCESS; -process(["mnesia", "info"]) -> +process(["mnesia", "info"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; -process(["mnesia", Arg]) -> +process(["mnesia", Arg], _Version) -> case catch mnesia:system_info(list_to_atom(Arg)) of {'EXIT', Error} -> print("Error: ~p~n", [Error]); Return -> print("~p~n", [Return]) @@ -172,23 +183,23 @@ process(["mnesia", Arg]) -> %% The arguments --long and --dual are not documented because they are %% automatically selected depending in the number of columns of the shell -process(["help" | Mode]) -> +process(["help" | Mode], Version) -> {MaxC, ShCode} = get_shell_info(), case Mode of [] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--dual"] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--long"] -> - print_usage(long, MaxC, ShCode), + print_usage(long, MaxC, ShCode, Version), ?STATUS_USAGE; ["--tags"] -> - print_usage_tags(MaxC, ShCode), + print_usage_tags(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["--tags", Tag] -> - print_usage_tags(Tag, MaxC, ShCode), + print_usage_tags(Tag, MaxC, ShCode, Version), ?STATUS_SUCCESS; ["help"] -> print_usage_help(MaxC, ShCode), @@ -196,13 +207,22 @@ process(["help" | Mode]) -> [CmdString | _] -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), - print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode), + print_usage_commands2(binary_to_list(CmdStringU), MaxC, ShCode, Version), ?STATUS_SUCCESS end; -process(Args) -> +process(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process(Args, Version); + +process(Args, Version) -> AccessCommands = get_accesscommands(), - {String, Code} = process2(Args, AccessCommands), + {String, Code} = process2(Args, AccessCommands, Version), case String of [] -> ok; _ -> @@ -211,18 +231,21 @@ process(Args) -> Code. %% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} -process2(["--auth", User, Server, Pass | Args], AccessCommands) -> - process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands); -process2(Args, AccessCommands) -> - process2(Args, noauth, AccessCommands). +process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> + process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), + list_to_binary(Pass), true}, Version); +process2(Args, AccessCommands, Version) -> + process2(Args, AccessCommands, admin, Version). + + -process2(Args, Auth, AccessCommands) -> - case try_run_ctp(Args, Auth, AccessCommands) of +process2(Args, AccessCommands, Auth, Version) -> + case try_run_ctp(Args, Auth, AccessCommands, Version) of {String, wrong_command_arguments} when is_list(String) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, - process(["help" | [CommandString]]), + process(["help" | [CommandString]], Version), {lists:flatten(String), ?STATUS_ERROR}; {String, Code} when is_list(String) and is_integer(Code) -> @@ -246,29 +269,29 @@ get_accesscommands() -> %%----------------------------- %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_run_ctp(Args, Auth, AccessCommands) -> +try_run_ctp(Args, Auth, AccessCommands, Version) -> try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of false when Args /= [] -> - try_call_command(Args, Auth, AccessCommands); + try_call_command(Args, Auth, AccessCommands, Version); false -> - print_usage(), + print_usage(Version), {"", ?STATUS_USAGE}; Status -> {"", Status} catch exit:Why -> - print_usage(), + print_usage(Version), {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; Error:Why -> %% In this case probably ejabberd is not started, so let's show Status - process(["status"]), + process(["status"], Version), print("~n", []), {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_call_command(Args, Auth, AccessCommands) -> - try call_command(Args, Auth, AccessCommands) of +try_call_command(Args, Auth, AccessCommands, Version) -> + try call_command(Args, Auth, AccessCommands, Version) of {error, command_unknown} -> {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; {error, wrong_command_arguments} -> @@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) -> Res -> Res catch + throw:Error -> + {io_lib:format("~p", [Error]), ?STATUS_ERROR}; A:Why -> Stack = erlang:get_stacktrace(), {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} end. %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType} -call_command([CmdString | Args], Auth, AccessCommands) -> +call_command([CmdString | Args], Auth, AccessCommands, Version) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), Command = list_to_atom(binary_to_list(CmdStringU)), - case ejabberd_commands:get_command_format(Command, Auth) of + case ejabberd_commands:get_command_format(Command, Auth, Version) of {error, command_unknown} -> {error, command_unknown}; {ArgsFormat, ResultFormat} -> case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, - ArgsFormatted), + Result = ejabberd_commands:execute_command(AccessCommands, + Auth, Command, + ArgsFormatted, + Version), format_result(Result, ResultFormat); {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {NumCompa, TextCompa} = @@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; make_status(_Error) -> ?STATUS_ERROR. -get_list_commands() -> - try ejabberd_commands:list_commands() of +get_list_commands(Version) -> + try ejabberd_commands:list_commands(Version) of Commands -> [tuple_command_help(Command) || {N,_,_}=Command <- Commands, @@ -458,10 +485,10 @@ get_list_ctls() -> -define(U2, "\e[24m"). -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). -print_usage() -> +print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), - print_usage(dual, MaxC, ShCode). -print_usage(HelpMode, MaxC, ShCode) -> + print_usage(dual, MaxC, ShCode, Version). +print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = [ {"status", [], "Get ejabberd status"}, @@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) -> {"restart", [], "Restart ejabberd"}, {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"}, {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ - get_list_commands() ++ + get_list_commands(Version) ++ get_list_ctls(), print( - ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ", + ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--version ", ?U("api_version"), "] [--auth ", ?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", ?U("command"), " [", ?U("options"), "]\n" "\n" @@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> %% Print Tags %%----------------------------- -print_usage_tags(MaxC, ShCode) -> +print_usage_tags(MaxC, ShCode, Version) -> print("Available tags and commands:", []), - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), lists:foreach( fun({Tag, Commands} = _TagCommands) -> print(["\n\n ", ?B(Tag), "\n "], []), @@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) -> TagsCommands), print("\n\n", []). -print_usage_tags(Tag, MaxC, ShCode) -> +print_usage_tags(Tag, MaxC, ShCode, Version) -> print(["Available commands with tag ", ?B(Tag), ":", "\n"], []), HelpMode = long, - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of {value, {Tag, CNs}} -> CNs; false -> [] @@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) -> CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( - list_to_atom(NameString)), + list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, @@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) -> %%----------------------------- %% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_commands(CmdSubString, MaxC, ShCode) -> +print_usage_commands2(CmdSubString, MaxC, ShCode, Version) -> %% Get which command names match this substring - AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()], + AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], Cmds = filter_commands(AllCommandsNames, CmdSubString), case Cmds of - [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]); - _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) + [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]); + _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version) end. -print_usage_commands2(Cmds, MaxC, ShCode) -> +print_usage_commands3(Cmds, MaxC, ShCode, Version) -> %% Then for each one print it lists:mapfoldl( fun(Cmd, Remaining) -> - print_usage_command(Cmd, MaxC, ShCode), + print_usage_command(Cmd, MaxC, ShCode, Version), case Remaining > 1 of true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []); false -> ok @@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) -> All). %% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_command(Cmd, MaxC, ShCode) -> +print_usage_command(Cmd, MaxC, ShCode, Version) -> Name = list_to_atom(Cmd), - case ejabberd_commands:get_command_definition(Name) of + case ejabberd_commands:get_command_definition(Name, Version) of command_not_found -> io:format("Error: command ~p not known.~n", [Cmd]); C -> - print_usage_command(Cmd, C, MaxC, ShCode) + print_usage_command2(Cmd, C, MaxC, ShCode) end. -print_usage_command(Cmd, C, MaxC, ShCode) -> +print_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, desc = Desc, diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index f0e56719..feff8af8 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -590,36 +590,35 @@ remove_node(Node) -> %%% set_password(User, Host, Password) -> - case ejabberd_auth:set_password(User, Host, Password) of - ok -> - ok; - _ -> - error - end. + Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end, + user_action(User, Host, Fun, ok). %% Copied some code from ejabberd_commands.erl check_password_hash(User, Host, PasswordHash, HashMethod) -> AccountPass = ejabberd_auth:get_password_s(User, Host), AccountPassHash = case {AccountPass, HashMethod} of {A, _} when is_tuple(A) -> scrammed; - {_, "md5"} -> get_md5(AccountPass); - {_, "sha"} -> get_sha(AccountPass); - _ -> undefined + {_, <<"md5">>} -> get_md5(AccountPass); + {_, <<"sha">>} -> get_sha(AccountPass); + {_, _Method} -> + ?ERROR_MSG("check_password_hash called " + "with hash method", [_Method]), + undefined end, case AccountPassHash of scrammed -> - ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []), + ?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []), throw(passwords_scrammed_command_cannot_work); - undefined -> error; + undefined -> throw(unkown_hash_method); PasswordHash -> ok; - _ -> error + _ -> false end. get_md5(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(erlang:md5(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(erlang:md5(AccountPass))]). get_sha(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(p1_sha:sha1(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(p1_sha:sha1(AccountPass))]). num_active_users(Host, Days) -> list_last_activity(Host, true, Days). @@ -782,7 +781,8 @@ resource_num(User, Host, Num) -> true -> lists:nth(Num, Resources); false -> - lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num])) + throw({bad_argument, + lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))}) end. kick_session(User, Server, Resource, ReasonText) -> @@ -1569,6 +1569,20 @@ decide_rip_jid({UName, UServer}, Match_list) -> end, Match_list). +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:is_user_exists(User, Server) of + true -> + case catch Fun() of + OK -> ok; + {error, Error} -> throw(Error); + Error -> + ?ERROR_MSG("Command returned: ~p", [Error]), + 1 + end; + false -> + throw({not_found, "unknown_user"}) + end. + %% Copied from ejabberd-2.0.0/src/acl.erl is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index c2b7d110..c4fae202 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -29,6 +29,11 @@ %% request_handlers: %% "/api": mod_http_api %% +%% To use a specific API version N, add a vN element in the URL path: +%% in ejabberd_http listener +%% request_handlers: +%% "/api/v2": mod_http_api +%% %% Access rights are defined with: %% commands_admin_access: configure %% commands: @@ -76,6 +81,8 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). +-define(DEFAULT_API_VERSION, 0). + -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). @@ -181,7 +188,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) -> true -> {allowed, Call, admin}; _ -> unauthorized_response() end; - _ -> + E -> + ?DEBUG("Unauthorized: ~p", [E]), unauthorized_response() end; check_permissions2(_Request, _Call, _Policy) -> @@ -196,10 +204,13 @@ oauth_check_token(Scope, Token) -> %% command processing %% ------------------ +%process(Call, Request) -> +% ?DEBUG("~p~n~p", [Call, Request]), ok; process(_, #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), - badrequest_response(); + badrequest_response(<<"Missing POST data">>); process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case jiffy:decode(Data) of List when is_list(List) -> List; @@ -209,33 +220,37 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), - badrequest_response() + catch _:{error,{_,invalid_json}} = _Err -> + ?DEBUG("Bad Request: ~p", [_Err]), + badrequest_response(<<"Invalid JSON input">>); + _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), + badrequest_response() end; process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case Data of - [{nokey, <<>>}] -> []; - _ -> Data - end, + [{nokey, <<>>}] -> []; + _ -> Data + end, log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), + catch _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; process([], #request{method = 'OPTIONS', data = <<>>}) -> @@ -244,13 +259,28 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), badrequest_response(). +% get API version N from last "vN" element in URL path +get_api_version(#request{path = Path}) -> + get_api_version(lists:reverse(Path)); +get_api_version([<<"v", String/binary>> | Tail]) -> + case catch jlib:binary_to_integer(String) of + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail) + end; +get_api_version([_Head | Tail]) -> + get_api_version(Tail); +get_api_version([]) -> + ?DEFAULT_API_VERSION. + %% ---------------- %% command handlers %% ---------------- % generic ejabberd command handler -handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> - case ejabberd_commands:get_command_format(Call, Auth) of +handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + case ejabberd_commands:get_command_format(Call, Auth, Version) of {ArgsSpec, _} when is_list(ArgsSpec) -> Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], Spec = lists:foldr( @@ -265,24 +295,48 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> ({Key, atom}, Acc) -> [{Key, undefined}|Acc] end, [], ArgsSpec), - handle2(Call, Auth, match(Args2, Spec)); + try + handle2(Call, Auth, match(Args2, Spec), Version) + catch throw:not_found -> + {404, <<"not_found">>}; + throw:{not_found, Why} when is_atom(Why) -> + {404, jlib:atom_to_binary(Why)}; + throw:{not_found, Msg} -> + {404, iolist_to_binary(Msg)}; + throw:not_allowed -> + {401, <<"not_allowed">>}; + throw:{not_allowed, Why} when is_atom(Why) -> + {401, jlib:atom_to_binary(Why)}; + throw:{not_allowed, Msg} -> + {401, iolist_to_binary(Msg)}; + throw:{error, account_unprivileged} -> + {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)}; + throw:{invalid_parameter, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:{error, Why} when is_atom(Why) -> + {400, jlib:atom_to_binary(Why)}; + throw:{error, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:Error when is_atom(Error) -> + {400, jlib:atom_to_binary(Error)}; + throw:Msg when is_list(Msg); is_binary(Msg) -> + {400, iolist_to_binary(Msg)}; + _Error -> + ?ERROR_MSG("REST API Error: ~p ~p", [_Error, erlang:get_stacktrace()]), + {500, <<"internal_error">>} + end; {error, Msg} -> + ?ERROR_MSG("REST API Error: ~p", [Msg]), {400, Msg}; _Error -> + ?ERROR_MSG("REST API Error: ~p", [_Error]), {400, <<"Error">>} end. -handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) -> - {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth), +handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), ArgsFormatted = format_args(Args, ArgsF), - case ejabberd_command(Auth, Call, ArgsFormatted, 400) of - 0 -> {200, <<"OK">>}; - 1 -> {500, <<"500 Internal server error">>}; - 400 -> {400, <<"400 Bad Request">>}; - 401 -> {401, <<"401 Unauthorized">>}; - 404 -> {404, <<"404 Not found">>}; - Res -> format_command_result(Call, Auth, Res) - end. + ejabberd_command(Auth, Call, ArgsFormatted, Version). get_elem_delete(A, L) -> case proplists:get_all_values(A, L) of @@ -346,7 +400,9 @@ format_arg(undefined, binary) -> <<>>; format_arg(undefined, string) -> <<>>; format_arg(Arg, Format) -> ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), - error. + throw({invalid_parameter, + io_lib:format("Arg ~p is not in format ~p", + [Arg, Format])}). process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); @@ -360,37 +416,37 @@ process_unicode_codepoints(Str) -> match(Args, Spec) -> [{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec]. -ejabberd_command(Auth, Cmd, Args, Default) -> +ejabberd_command(Auth, Cmd, Args, Version) -> Access = case Auth of admin -> []; _ -> undefined end, - case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of - {'EXIT', _} -> Default; - {error, account_unprivileged} -> 401; - {error, _} -> Default; - Result -> Result + case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Cmd, Auth, Res, Version) end. -format_command_result(Cmd, Auth, Result) -> - {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth), +format_command_result(Cmd, Auth, Result, Version) -> + {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of - {{_, rescode}, V} when V == true; V == ok -> - {200, <<"">>}; - {{_, rescode}, _} -> - {500, <<"">>}; - {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> - {200, iolist_to_binary(Text1)}; - {{_, restuple}, {_, Text2}} -> - {500, iolist_to_binary(Text2)}; - {{_, {list, _}}, _V} -> - {_, L} = format_result(Result, ResultFormat), - {200, L}; - {{_, {tuple, _}}, _V} -> - {_, T} = format_result(Result, ResultFormat), - {200, T}; - _ -> - {200, {[format_result(Result, ResultFormat)]}} + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; + {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> + {200, iolist_to_binary(Text1)}; + {{_, restuple}, {_, Text2}} -> + {500, iolist_to_binary(Text2)}; + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> + {200, {[format_result(Result, ResultFormat)]}} end. format_result(Atom, {Name, atom}) -> @@ -429,14 +485,15 @@ format_result(404, {_Name, _}) -> "not_found". unauthorized_response() -> - {401, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"401 Unauthorized">>}]}}. + unauthorized_response(<<"401 Unauthorized">>). +unauthorized_response(Body) -> + json_response(401, jiffy:encode(Body)). badrequest_response() -> - {400, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"400 Bad Request">>}]}}. + badrequest_response(<<"400 Bad Request">>). +badrequest_response(Body) -> + json_response(400, jiffy:encode(Body)). + json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. |