diff options
Diffstat (limited to 'src/ejabberd_xmlrpc.erl')
-rw-r--r-- | src/ejabberd_xmlrpc.erl | 359 |
1 files changed, 105 insertions, 254 deletions
diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 6680451e4..9b4b119b6 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -5,7 +5,7 @@ %%% Created : 21 Aug 2007 by Badlop <badlop@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2016 ProcessOne +%%% ejabberd, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,211 +23,53 @@ %%% %%%---------------------------------------------------------------------- -%%% 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). +-behaviour(ejabberd_listener). -author('badlop@process-one.net'). --export([start/2, handler/2, process/2, socket_type/0, - transform_listen_option/2]). +-export([start/3, start_link/3, handler/2, process/2, accept/1, + listen_options/0]). --include("ejabberd.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("mod_roster.hrl"). --include("jlib.hrl"). +-include("xmpp.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"}}} + auth = noauth :: noauth | map(), + get_auth = true :: boolean(), + ip :: inet:ip_address()}). %% ----------------------------- %% Listener interface %% ----------------------------- -start({gen_tcp = _SockMod, Socket}, Opts) -> - ejabberd_http:start({gen_tcp, Socket}, [{xmlrpc, true}|Opts]). +start(SockMod, Socket, Opts) -> + Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts], + ejabberd_http:start(SockMod, Socket, Opts1). + +start_link(SockMod, Socket, Opts) -> + Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts], + ejabberd_http:start_link(SockMod, Socket, Opts1). -socket_type() -> raw. +accept(Pid) -> + ejabberd_http:accept(Pid). %% ----------------------------- %% HTTP interface %% ----------------------------- -process(_, #request{method = 'POST', data = Data, opts = Opts}) -> - AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts, - fun(L) when is_list(L) -> L end, - undefined), - AccessCommands = - case AccessCommandsOpts of - undefined -> undefined; - _ -> - 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) - end, + +process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) -> + AccessCommands = proplists:get_value(access_commands, Opts, []), GetAuth = true, - State = #state{access_commands = AccessCommands, get_auth = GetAuth}, + State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP}, case fxml_stream:parse_element(Data) of {error, _} -> {400, [], @@ -236,13 +78,13 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) -> El -> case fxmlrpc:decode(El) of {error, _} = Err -> - ?ERROR_MSG("XML-RPC request ~s failed with reason: ~p", + ?ERROR_MSG("XML-RPC request ~ts failed with reason: ~p", [Data, Err]), {400, [], #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"Malformed Request">>}]}}; {ok, RPC} -> - ?DEBUG("got XML-RPC request: ~p", [RPC]), + ?DEBUG("Got XML-RPC request: ~p", [RPC]), {false, Result} = handler(State, RPC), XML = fxml:element_to_binary(fxmlrpc:encode(Result)), {200, [{<<"Content-Type">>, <<"text/xml">>}], @@ -258,21 +100,37 @@ process(_, _) -> %% Access verification %% ----------------------------- -get_auth(AuthList) -> - Admin = - case lists:keysearch(admin, 1, AuthList) of - {value, {admin, true}} -> true; - _ -> false - end, +-spec extract_auth([{user | server | token | password, binary()}]) -> + map() | {error, not_found | expired | invalid_auth}. +extract_auth(AuthList) -> + ?DEBUG("AUTHLIST ~p", [AuthList]), try get_attrs([user, server, token], AuthList) of - [U, S, T] -> {U, S, {oauth, T}, Admin} + [U0, S0, T] -> + U = jid:nodeprep(U0), + S = jid:nameprep(S0), + case ejabberd_oauth:check_token(T) of + {ok, {U, S}, Scope} -> + #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; + {false, Reason} -> + {error, Reason}; + _ -> + {error, not_found} + end catch - exit:{attribute_not_found, _Attr, _} -> + exit:{attribute_not_found, _, _} -> try get_attrs([user, server, password], AuthList) of - [U, S, P] -> {U, S, P, Admin} + [U0, S0, P] -> + U = jid:nodeprep(U0), + S = jid:nameprep(S0), + case ejabberd_auth:check_password(U, <<"">>, S, P) of + true -> + #{usr => {U, S, <<"">>}, caller_server => S}; + false -> + {error, invalid_auth} + end catch - exit:{attribute_not_found, Attr, _} -> - throw({error, missing_auth_arguments, Attr}) + exit:{attribute_not_found, Attr, _} -> + throw({error, missing_auth_arguments, Attr}) end end. @@ -280,70 +138,60 @@ get_auth(AuthList) -> %% 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, +handler(#state{get_auth = true, auth = noauth, ip = IP} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) -> - try get_auth(AuthList) of + try extract_auth(AuthList) of + {error, invalid_auth} -> + build_fault_response(-118, + "Invalid authentication data", + []); + {error, not_found} -> + build_fault_response(-118, + "Invalid oauth token", + []); + {error, expired} -> + build_fault_response(-118, + "Invalid oauth token", + []); Auth -> - handler(State#state{get_auth = false, auth = Auth}, + handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}}, {call, Method, Arguments}) catch {error, missing_auth_arguments, _Attr} -> - handler(State#state{get_auth = false, auth = noauth}, + handler(State#state{get_auth = false, + auth = #{ip => IP, caller_module => ?MODULE}}, {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, State#state.auth) 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 + {call, Command, [{struct, AttrL}]}) -> + {ArgsF, ArgsR, ResultF} = ejabberd_commands:get_command_format(Command, State#state.auth), + try_do_command(State#state.access_commands, + State#state.auth, Command, AttrL, ArgsF, ArgsR, ResultF); handler(_State, Payload) -> build_fault_response(-112, "Unknown call: ~p", [Payload]). @@ -353,9 +201,9 @@ handler(_State, Payload) -> %% ----------------------------- try_do_command(AccessCommands, Auth, Command, AttrL, - ArgsF, ResultF) -> + ArgsF, ArgsR, ResultF) -> try do_command(AccessCommands, Auth, Command, AttrL, - ArgsF, ResultF) + ArgsF, ArgsR, ResultF) of {command_result, ResultFormatted} -> {false, {response, [ResultFormatted]}} @@ -390,15 +238,25 @@ build_fault_response(Code, ParseString, ParseArgs) -> ?WARNING_MSG(FaultString, []), {false, {response, {fault, Code, list_to_binary(FaultString)}}}. -do_command(AccessCommands, Auth, Command, AttrL, ArgsF, +do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ArgsR, ResultF) -> - ArgsFormatted = format_args(AttrL, ArgsF), - Result = - ejabberd_commands:execute_command(AccessCommands, Auth, - Command, ArgsFormatted), + ArgsFormatted = format_args(rename_old_args(AttrL, ArgsR), ArgsF), + Auth2 = Auth#{extra_permissions => AccessCommands}, + Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2), ResultFormatted = format_result(Result, ResultF), {command_result, ResultFormatted}. +rename_old_args(Args, []) -> + Args; +rename_old_args(Args, [{OldName, NewName} | ArgsR]) -> + Args2 = case lists:keytake(OldName, 1, Args) of + {value, {OldName, Value}, ArgsTail} -> + [{NewName, Value} | ArgsTail]; + false -> + Args + end, + rename_old_args(Args2, ArgsR). + %%----------------------------- %% Format arguments %%----------------------------- @@ -410,7 +268,6 @@ 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. @@ -418,10 +275,8 @@ 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. @@ -475,7 +330,7 @@ format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); 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_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]), exit({invalid_arg_type, Arg, Format}). process_unicode_codepoints(Str) -> @@ -487,8 +342,14 @@ process_unicode_codepoints(Str) -> %% Result %% ----------------------------- +format_result({error, Error}, _) when is_list(Error) -> + throw({error, lists:flatten(Error)}); format_result({error, Error}, _) -> throw({error, Error}); +format_result({error, _Type, _Code, Error}, _) when is_list(Error) -> + throw({error, lists:flatten(Error)}); +format_result({error, _Type, _Code, Error}, _) -> + throw({error, Error}); format_result(String, string) -> lists:flatten(String); format_result(Atom, {Name, atom}) -> {struct, @@ -514,15 +375,13 @@ format_result(Code, {Name, rescode}) -> format_result({Code, Text}, {Name, restuple}) -> {struct, [{Name, make_status(Code)}, - {text, lists:flatten(Text)}]}; -%% Result is a list of something: [something()] + {text, io_lib:format("~ts", [Text])}]}; 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), @@ -541,13 +400,5 @@ 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]. +listen_options() -> + []. |