From 3dc55c6d47e3093a6147ce275c7269a7d08ffc45 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Thu, 31 Mar 2016 14:53:31 +0300 Subject: Commands refactor, first pass. - add API versionning - changed error handling, based on exception - commands moved/merged from mod_admin_p1 to mod_admin_extra - command bufixes - add some elixir unit test cases Squashed commit of the following: commit dd59855b3486f78a9349756e4f102e79b3accff8 Merge: 14e8ffc 506e08e Author: Jerome Sautret Date: Fri Oct 30 11:43:18 2015 +0100 Merge branch '3.2.x' into api commit 14e8ffce78cbea6c8605371d1fc50a0c1d1e012c Author: Jerome Sautret Date: Tue Oct 27 16:35:17 2015 +0100 Added OAuth tests to ejabberd_commands commit f81c550c14628edfe4861c228576cb767924366a Author: Jerome Sautret Date: Tue Oct 27 16:34:55 2015 +0100 Added some mod_http_api tests commit 6a64578d5b2ba532a2feb6503ed98561e56d5d53 Author: Jerome Sautret Date: Mon Oct 26 15:29:36 2015 +0100 Fix get_last command test Previous version won't work with dst. commit 27e0cde9e9c1f001effe68f8424a365ad947c068 Author: Jerome Sautret Date: Fri Oct 23 17:59:34 2015 +0200 Add tests on admin command policy commit 19dad8d54f54c9fabd454280483cccfb06c8e78a Author: Jerome Sautret Date: Fri Oct 23 16:49:36 2015 +0200 Added command related tests (http api & user policy) commit e0e596ab4a3f3a70aba5f374f028939ab794de33 Author: Jerome Sautret Date: Fri Oct 23 16:49:16 2015 +0200 Fix command call. commit 128cd7d1ede3c47a34f8ec3a750c980ccad2c61d Merge: 60c4c4c 447313c Author: Jerome Sautret Date: Thu Oct 22 14:48:39 2015 +0200 Merge branch '3.2.x' into api commit 60c4c4c0751302524c14219c6bc8c56a6069a689 Author: Jerome Sautret Date: Thu Oct 22 14:45:57 2015 +0200 Fix ejabberd_commands spec. commit 8e145c28c5da762c2b93ee32327eff1db94ebfed Merge: 397273a f13dc94 Author: Jerome Sautret Date: Wed Oct 21 18:26:07 2015 +0200 Merge branch '3.2.x' into api commit 397273a23ed415feac87aed33da6452229793387 Merge: c30e89b f289e27 Author: Jerome Sautret Date: Wed Oct 21 15:27:45 2015 +0200 Merge branch '3.2.x' into api commit c30e89bb8a0013bff37e61e4c6953350c9c1f313 Author: Jerome Sautret Date: Wed Oct 21 12:47:02 2015 +0200 Merge mod_http_api commit 7b0db22b4acd48ff6fabce41c1b2525e6580a3c5 Author: Jerome Sautret Date: Fri Oct 16 11:55:48 2015 +0200 Fix exunit tests to run with common_test suites commit d8b1a89800ac7379a57a7eb4a09c3c93c3e1e5eb Merge: 2879ae8 63455b3 Author: Jerome Sautret Date: Thu Oct 15 11:39:45 2015 +0200 Merge branch '3.2.x' into api commit 2879ae87ff3eee369ef3d780136b96ecff5285d1 Author: Jerome Sautret Date: Wed Oct 14 14:53:44 2015 +0200 Fix update_roster command. commit a1d453dd7a3afda9861a8d747494a45057ad574b Author: Jerome Sautret Date: Tue Oct 13 16:14:28 2015 +0200 API commands refactor Moving and/or merging commands from mod_admin_p1 to mod_admin_extra commit b709ed26b0fc0ca4f3bdd5a59fa58ec7e3db97fa Author: Jerome Sautret Date: Wed Oct 7 15:10:01 2015 +0200 Add tests on commands commit 6711687bee9c672cb3d5aed0744e13420ecf6dbd Author: Jerome Sautret Date: Tue Sep 29 15:58:16 2015 +0200 Add ejabberd_commands tests commit df8682f419cf3877e77e36a19bca0fc55dc991f8 Author: Jerome Sautret Date: Mon Sep 28 14:54:39 2015 +0200 Added API versioning for ejabberdctl and rest commands commit cd017b0e3aac431bc3ee807ceb7f8641e1523ef5 Author: Jerome Sautret Date: Fri Sep 18 11:21:45 2015 +0200 Better error handling of HTTP API commands. commit ca5cb6acd8e4643f9d6c484d2277b0d7e88471e5 Author: Jerome Sautret Date: Tue Sep 15 15:03:05 2015 +0200 add commands to mod_admin_extra: - get_offline_count - get_presence - change_password commit 7f583fa099e30ac2b0915669fd8f102ac565b833 Author: Jerome Sautret Date: Tue Sep 15 15:02:16 2015 +0200 Improve REST API error handling commit 14753b1c02cdce434a786b7f80f6c09f0d210075 Author: Jerome Sautret Date: Mon Sep 14 10:51:17 2015 +0200 Change REST API return codes for integer type. --- src/mod_http_api.erl | 160 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 54 deletions(-) (limited to 'src/mod_http_api.erl') diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 15fe36364..f2b7a484b 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">>}). @@ -179,7 +186,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call) -> true -> {allowed, Call, admin}; _ -> unauthorized_response() end; - _ -> + _E -> + ?DEBUG("Unauthorized: ~p", [_E]), unauthorized_response() end. @@ -192,10 +200,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; @@ -205,16 +216,20 @@ 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)); ErrorResponse -> %% Should we reply 403 ? ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), + 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, <<>>}] -> []; @@ -223,13 +238,13 @@ process([Call], #request{method = 'GET', q = 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)); 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 = <<>>}) -> @@ -238,13 +253,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( @@ -259,22 +289,51 @@ 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:{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">>}; - 404 -> {404, <<"404 Not found">>}; - Res -> format_command_result(Call, Auth, Res) + case ejabberd_commands:execute_command(undefined, Auth, + Call, ArgsFormatted, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Call, Auth, Res, Version) end. get_elem_delete(A, L) -> @@ -339,7 +398,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]); @@ -353,36 +414,26 @@ process_unicode_codepoints(Str) -> match(Args, Spec) -> [{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec]. -ejabberd_command(Auth, Cmd, Args, Default) -> - Access = case Auth of - admin -> []; - _ -> undefined - end, - case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of - {'EXIT', _} -> Default; - {error, _} -> Default; - Result -> Result - 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}) -> @@ -421,14 +472,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}. -- cgit v1.2.3