]> granicus.if.org Git - ejabberd/commitdiff
Add ejabberd_xmlrpc
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 10 Dec 2013 11:25:12 +0000 (21:25 +1000)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 10 Dec 2013 11:25:12 +0000 (21:25 +1000)
src/ejabberd_xmlrpc.erl [new file with mode: 0644]

diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl
new file mode 100644 (file)
index 0000000..40223f7
--- /dev/null
@@ -0,0 +1,484 @@
+%%%----------------------------------------------------------------------
+%%% File    : ejabberd_xmlrpc.erl
+%%% Author  : Badlop <badlop@process-one.net>
+%%% Purpose : XML-RPC server that frontends ejabberd commands
+%%% Created : 21 Aug 2007 by Badlop <badlop@ono.com>
+%%% Id      : $Id: ejabberd_xmlrpc.erl 595 2008-05-20 11:39:31Z badlop $
+%%%----------------------------------------------------------------------
+
+%%% TODO: Implement a command in ejabberdctl 'help COMMAND LANGUAGE' that shows
+%%% a coding example to call that command in a specific language (python, php).
+
+%%% TODO: Remove support for plaintext password
+
+%%% TODO: commands strings should be strings without ~n
+
+-module(ejabberd_xmlrpc).
+
+-author('badlop@process-one.net').
+
+-export([start/2, handler/2, socket_type/0, transform_listen_option/2]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+
+-include("mod_roster.hrl").
+
+-include("jlib.hrl").
+
+-record(state,
+       {access_commands = [] :: list(),
+         auth = noauth        :: noauth | {binary(), binary(), binary()},
+         get_auth = true      :: boolean()}).
+
+%% Test:
+
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_integer, [{struct, [{thisinteger, 5}]}]}).
+%% {ok,{response,[{struct,[{zero,0}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_string, [{struct, [{thisstring, "abcd"}]}]}).
+%% {ok,{response,[{struct,[{thatstring,"abcd"}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, tell_tuple_3integer, [{struct, [{thisstring, "abcd"}]}]}).
+%% {ok,{response,
+%%         [{struct,
+%%              [{thattuple,
+%%                   {array,
+%%                       [{struct,[{first,123}]},
+%%                        {struct,[{second,456}]},
+%%                        {struct,[{third,789}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, pow, [{struct, [{base, 5}, {exponent, 7}]}]}).
+%% {ok,{response,[{struct,[{pow,78125}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, seq, [{struct, [{from, 3}, {to, 7}]}]}).
+%% {ok,{response,[{array,[{struct,[{intermediate,3}]},
+%%                        {struct,[{intermediate,4}]},
+%%                        {struct,[{intermediate,5}]},
+%%                        {struct,[{intermediate,6}]},
+%%                        {struct,[{intermediate,7}]}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, substrs, [{struct, [{word, "abcd"}]}]}).
+%% NO:
+%% {ok,{response,[{array,[{struct,[{miniword,"a"}]},
+%%                        {struct,[{miniword,"ab"}]},
+%%                        {struct,[{miniword,"abc"}]},
+%%                        {struct,[{miniword,"abcd"}]}]}]}}
+%% {ok,{response,
+%%         [{struct,
+%%              [{substrings,
+%%                   {array,
+%%                       [{struct,[{miniword,"a"}]},
+%%                        {struct,[{miniword,"ab"}]},
+%%                        {struct,[{miniword,"abc"}]},
+%%                        {struct,[{miniword,"abcd"}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, splitjid, [{struct, [{jid, "abcd@localhost/work"}]}]}).
+%% {ok,{response,
+%%         [{struct,
+%%              [{jidparts,
+%%                   {array,
+%%                       [{struct,[{user,"abcd"}]},
+%%                        {struct,[{server,"localhost"}]},
+%%                        {struct,[{resource,"work"}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 55}]}]}).
+%% {ok,{response,
+%%         [{struct,
+%%              [{thistuple,
+%%                   {array,
+%%                       [{struct,[{thisinteger,55}]},
+%%                        {struct,[{thisstring,"abc"}]}]}}]}]}}
+%%
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_list_integer, [{struct, [{thislist, {array, [{struct, [{thisinteger, 55}, {thisinteger, 4567}]}]}}]}]}).
+%% {ok,{response,
+%%         [{struct,
+%%              [{thatlist,
+%%                   {array,
+%%                       [{struct,[{thatinteger,55}]},
+%%                        {struct,[{thatinteger,4567}]}]}}]}]}}
+%%
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_list_string, [{struct, [{thisinteger, 123456}, {thislist, {array, [{struct, [{thisstring, "abc"}, {thisstring, "bobo baba"}]}]}}]}]}).
+%% {ok,
+%%  {response,
+%%   [{struct,
+%%     [{thistuple,
+%%       {array,
+%%        [{struct,[{thatinteger,123456}]},
+%%         {struct,
+%%          [{thatlist,
+%%            {array,
+%%             [{struct,[{thatstring,"abc"}]},
+%%              {struct,[{thatstring,"bobo baba"}]}]}}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger2, 4567}]}]}}]}]}).
+%% {ok,{response,[{struct,[{zero,0}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_isatils, [{struct,
+%%                                           [{thisinteger, 123456990},
+%%                                                    {thisstring, "This is ISATILS"},
+%%                                                    {thisatom, "test_isatils"},
+%%                                                    {thistuple, {array, [{struct, [
+%%                                            {listlen, 2},
+%%                                                       {thislist, {array, [{struct, [
+%%                                               {contentstring, "word1"},
+%%                                               {contentstring, "word 2"}
+%%                                            ]}]}}
+%%                                                                                ]}]}}
+%%                                                                               ]}]}).
+%% {ok,{response,
+%%         [{struct,
+%%              [{results,
+%%                   {array,
+%%                       [{struct,[{thatinteger,123456990}]},
+%%                        {struct,[{thatstring,"This is ISATILS"}]},
+%%                        {struct,[{thatatom,"test_isatils"}]},
+%%                        {struct,
+%%                            [{thattuple,
+%%                                 {array,
+%%                                     [{struct,[{listlen,123456990}]},
+%%                                      {struct,[{thatlist,...}]}]}}]}]}}]}]}}
+
+%% ecommand doesn't exist:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string2, [{struct, [{thisstring, "abc"}]}]}).
+%% {ok,{response,{fault,-1, "Unknown call: {call,echo_integer_string2,[{struct,[{thisstring,\"abc\"}]}]}"}}}
+%%
+%% Duplicated argument:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 44}, {thisinteger, 55}]}]}).
+%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger' duplicated:\n[{thisstring,\"abc\"},{thisinteger,44},{thisinteger,55}]"}}}
+%%
+%% Missing argument:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}]}]}).
+%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger' not found:\n[{thisstring,\"abc\"}]"}}}
+%%
+%% Duplicated tuple element:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger1, 66}, {thisinteger2, 4567}]}]}}]}]}).
+%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger1' defined multiple times:\n[{thisinteger1,55},{thisinteger1,66},{thisinteger2,4567}]"}}}
+%%
+%% Missing element in tuple:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisintegerc, 66}, {thisinteger, 4567}]}]}}]}]}).
+%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger2' not found:\n[{thisintegerc,66},{thisinteger,4567}]"}}}
+%%
+%% The ecommand crashed:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, this_crashes, [{struct, []}]}).
+%% {ok,{response,{fault,-100, "Error -100\nA problem 'error' occurred executing the command this_crashes with arguments []: badarith"}}}
+
+%% -----------------------------
+%% Listener interface
+%% -----------------------------
+
+start({gen_tcp = _SockMod, Socket}, Opts) ->
+    %MaxSessions = gen_mod:get_opt(maxsessions, Opts,
+    %                              fun(I) when is_integer(I), I>0 -> I end,
+    %                              10),
+    Timeout = gen_mod:get_opt(timeout, Opts,
+                              fun(I) when is_integer(I), I>0 -> I end,
+                              5000),
+    AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
+                                         fun(L) when is_list(L) -> L end,
+                                         []),
+    AccessCommands = lists:flatmap(
+                       fun({Ac, AcOpts}) ->
+                               Commands = gen_mod:get_opt(
+                                            commands, AcOpts,
+                                            fun(A) when is_atom(A) ->
+                                                    A;
+                                               (L) when is_list(L) ->
+                                                    true = lists:all(
+                                                             fun is_atom/1,
+                                                             L),
+                                                    L
+                                            end, all),
+                               CommOpts = gen_mod:get_opt(
+                                            options, AcOpts,
+                                            fun(L) when is_list(L) -> L end,
+                                            []),
+                               [{Ac, Commands, CommOpts}];
+                          (Wrong) ->
+                               ?WARNING_MSG("wrong options format for ~p: ~p",
+                                            [?MODULE, Wrong]),
+                               []
+                       end, AccessCommandsOpts),
+    GetAuth = case [ACom
+                   || {Ac, _, _} = ACom <- AccessCommands, Ac /= all]
+                 of
+               [] -> false;
+               _ -> true
+             end,
+    Handler = {?MODULE, handler},
+    State = #state{access_commands = AccessCommands,
+                  get_auth = GetAuth},
+    Pid = proc_lib:spawn(xmlrpc_http, handler, [Socket, Timeout, Handler, State]),
+    {ok, Pid}.
+
+socket_type() -> raw.
+
+%% -----------------------------
+%% Access verification
+%% -----------------------------
+
+get_auth(AuthList) ->
+    [User, Server, Password] = try get_attrs([user, server,
+                                             password],
+                                            AuthList)
+                              of
+                                [U, S, P] -> [U, S, P]
+                              catch
+                                exit:{attribute_not_found, Attr, _} ->
+                                    throw({error, missing_auth_arguments,
+                                           Attr})
+                              end,
+    {User, Server, Password}.
+
+%% -----------------------------
+%% Handlers
+%% -----------------------------
+
+%% Call:           Arguments:                                      Returns:
+
+%% .............................
+%%  Access verification
+
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [152]}).
+%% {ok,{response,{fault,-103, "Error -103\nRequired authentication: {call,echothis,[152]}"}}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada"}]}, 152]}).
+%% {ok,{response,{fault,-103,
+%%      "Error -103\nAuthentication non valid: [{user,\"badlop\"},\n
+%%      {server,\"localhost\"},\n
+%%      {password,\"ada\"}]"}}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada90ada"}]}, 152]}).
+%% {ok,{response,[152]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
+%% {ok,{response,[152]}}
+
+handler(#state{get_auth = true, auth = noauth} = State,
+       {call, Method,
+        [{struct, AuthList} | Arguments] = AllArgs}) ->
+    try get_auth(AuthList) of
+      Auth ->
+         handler(State#state{get_auth = false, auth = Auth},
+                 {call, Method, Arguments})
+    catch
+      {error, missing_auth_arguments, _Attr} ->
+         handler(State#state{get_auth = false, auth = noauth},
+                 {call, Method, AllArgs})
+    end;
+%% .............................
+%%  Debug
+%% echothis       String                                          String
+handler(_State, {call, echothis, [A]}) ->
+    {false, {response, [A]}};
+%% echothisnew    struct[{sentence, String}]   struct[{repeated, String}]
+handler(_State,
+       {call, echothisnew, [{struct, [{sentence, A}]}]}) ->
+    {false, {response, [{struct, [{repeated, A}]}]}};
+%% multhis        struct[{a, Integer}, {b, Integer}]              Integer
+handler(_State,
+       {call, multhis, [{struct, [{a, A}, {b, B}]}]}) ->
+    {false, {response, [A * B]}};
+%% multhisnew     struct[{a, Integer}, {b, Integer}]    struct[{mu, Integer}]
+handler(_State,
+       {call, multhisnew, [{struct, [{a, A}, {b, B}]}]}) ->
+    {false, {response, [{struct, [{mu, A * B}]}]}};
+%% .............................
+%% ejabberd commands
+handler(State, {call, Command, []}) ->
+    handler(State, {call, Command, [{struct, []}]});
+handler(State,
+       {call, Command, [{struct, AttrL}]} = Payload) ->
+    case ejabberd_commands:get_command_format(Command) of
+      {error, command_unknown} ->
+         build_fault_response(-112, "Unknown call: ~p",
+                              [Payload]);
+      {ArgsF, ResultF} ->
+         try_do_command(State#state.access_commands,
+                        State#state.auth, Command, AttrL, ArgsF, ResultF)
+    end;
+%% If no other guard matches
+handler(_State, Payload) ->
+    build_fault_response(-112, "Unknown call: ~p",
+                        [Payload]).
+
+%% -----------------------------
+%% Command
+%% -----------------------------
+
+try_do_command(AccessCommands, Auth, Command, AttrL,
+              ArgsF, ResultF) ->
+    try do_command(AccessCommands, Auth, Command, AttrL,
+                  ArgsF, ResultF)
+    of
+      {command_result, ResultFormatted} ->
+         {false, {response, [ResultFormatted]}}
+    catch
+      exit:{duplicated_attribute, ExitAt, ExitAtL} ->
+         build_fault_response(-114,
+                              "Attribute '~p' duplicated:~n~p",
+                              [ExitAt, ExitAtL]);
+      exit:{attribute_not_found, ExitAt, ExitAtL} ->
+         build_fault_response(-116,
+                              "Required attribute '~p' not found:~n~p",
+                              [ExitAt, ExitAtL]);
+      exit:{additional_unused_args, ExitAtL} ->
+         build_fault_response(-120,
+                              "The call provided additional unused "
+                                "arguments:~n~p",
+                              [ExitAtL]);
+      Why ->
+         build_fault_response(-118,
+                              "A problem '~p' occurred executing the "
+                                "command ~p with arguments~n~p",
+                              [Why, Command, AttrL])
+    end.
+
+build_fault_response(Code, ParseString, ParseArgs) ->
+    FaultString = "Error " ++ integer_to_list(Code) ++ "\n"
+        ++ lists:flatten(io_lib:format(ParseString, ParseArgs)),
+    ?WARNING_MSG(FaultString, []),
+    {false, {response, {fault, Code, FaultString}}}.
+
+do_command(AccessCommands, Auth, Command, AttrL, ArgsF,
+          ResultF) ->
+    ArgsFormatted = format_args(AttrL, ArgsF),
+    Result =
+       ejabberd_commands:execute_command(AccessCommands, Auth,
+                                         Command, ArgsFormatted),
+    ResultFormatted = format_result(Result, ResultF),
+    {command_result, ResultFormatted}.
+
+%%-----------------------------
+%% Format arguments
+%%-----------------------------
+
+get_attrs(Attribute_names, L) ->
+    [get_attr(A, L) || A <- Attribute_names].
+
+get_attr(A, L) ->
+    case lists:keysearch(A, 1, L) of
+      {value, {A, Value}} -> Value;
+      false ->
+         %% Report the error and then force a crash
+         exit({attribute_not_found, A, L})
+    end.
+
+get_elem_delete(A, L) ->
+    case proplists:get_all_values(A, L) of
+      [Value] -> {Value, proplists:delete(A, L)};
+      [_, _ | _] ->
+         %% Crash reporting the error
+         exit({duplicated_attribute, A, L});
+      [] ->
+         %% Report the error and then force a crash
+         exit({attribute_not_found, A, L})
+    end.
+
+format_args(Args, ArgsFormat) ->
+    {ArgsRemaining, R} = lists:foldl(fun ({ArgName,
+                                          ArgFormat},
+                                         {Args1, Res}) ->
+                                            {ArgValue, Args2} =
+                                                get_elem_delete(ArgName,
+                                                                Args1),
+                                            Formatted = format_arg(ArgValue,
+                                                                   ArgFormat),
+                                            {Args2, Res ++ [Formatted]}
+                                    end,
+                                    {Args, []}, ArgsFormat),
+    case ArgsRemaining of
+      [] -> R;
+      L when is_list(L) -> exit({additional_unused_args, L})
+    end.
+
+format_arg({array, Elements},
+          {list, {ElementDefName, ElementDefFormat}})
+    when is_list(Elements) ->
+    lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
+                        ElementDefName == ElementName ->
+                     format_arg(ElementValue, ElementDefFormat)
+             end,
+             Elements);
+format_arg({array, [{struct, Elements}]},
+          {list, {ElementDefName, ElementDefFormat}})
+    when is_list(Elements) ->
+    lists:map(fun ({ElementName, ElementValue}) ->
+                     true = ElementDefName == ElementName,
+                     format_arg(ElementValue, ElementDefFormat)
+             end,
+             Elements);
+format_arg({array, [{struct, Elements}]},
+          {tuple, ElementsDef})
+    when is_list(Elements) ->
+    FormattedList = format_args(Elements, ElementsDef),
+    list_to_tuple(FormattedList);
+format_arg({array, Elements}, {list, ElementsDef})
+    when is_list(Elements) and is_atom(ElementsDef) ->
+    [format_arg(Element, ElementsDef)
+     || Element <- Elements];
+format_arg(Arg, integer) when is_integer(Arg) -> Arg;
+format_arg(Arg, binary) when is_list(Arg) -> list_to_binary(Arg);
+format_arg(Arg, binary) when is_binary(Arg) -> Arg;
+format_arg(Arg, string) when is_binary(Arg) -> Arg.
+
+%% -----------------------------
+%% Result
+%% -----------------------------
+
+format_result({error, Error}, _) ->
+    throw({error, Error});
+format_result(String, string) -> lists:flatten(String);
+format_result(Atom, {Name, atom}) ->
+    {struct,
+     [{Name, iolist_to_binary(atom_to_list(Atom))}]};
+format_result(Int, {Name, integer}) ->
+    {struct, [{Name, Int}]};
+format_result(String, {Name, string}) when is_list(String) ->
+    {struct, [{Name, lists:flatten(String)}]};
+format_result(Binary, {Name, string}) when is_binary(Binary) ->
+    {struct, [{Name, binary_to_list(Binary)}]};
+format_result(Code, {Name, rescode}) ->
+    {struct, [{Name, make_status(Code)}]};
+format_result({Code, Text}, {Name, restuple}) ->
+    {struct,
+     [{Name, make_status(Code)},
+      {text, lists:flatten(Text)}]};
+%% Result is a list of something: [something()]
+format_result(Elements, {Name, {list, ElementsDef}}) ->
+    FormattedList = lists:map(fun (Element) ->
+                                     format_result(Element, ElementsDef)
+                             end,
+                             Elements),
+    {struct, [{Name, {array, FormattedList}}]};
+%% Result is a tuple with several elements: {something1(), something2(), ...}
+format_result(ElementsTuple,
+             {Name, {tuple, ElementsDef}}) ->
+    ElementsList = tuple_to_list(ElementsTuple),
+    ElementsAndDef = lists:zip(ElementsList, ElementsDef),
+    FormattedList = lists:map(fun ({Element, ElementDef}) ->
+                                     format_result(Element, ElementDef)
+                             end,
+                             ElementsAndDef),
+    {struct, [{Name, {array, FormattedList}}]};
+format_result(404, {Name, _}) ->
+    {struct, [{Name, make_status(not_found)}]}.
+
+make_status(ok) -> 0;
+make_status(true) -> 0;
+make_status(false) -> 1;
+make_status(error) -> 1;
+make_status(_) -> 1.
+
+transform_listen_option({access_commands, ACOpts}, Opts) ->
+    NewACOpts = lists:map(
+                  fun({AName, ACmds, AOpts}) ->
+                          {AName, [{commands, ACmds}, {options, AOpts}]};
+                     (Opt) ->
+                          Opt
+                  end, ACOpts),
+    [{access_commands, NewACOpts}|Opts];
+transform_listen_option(Opt, Opts) ->
+    [Opt|Opts].