2016-09-27 04:57:14 +02:00
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% File : rest.erl
|
|
|
|
%%% Author : Christophe Romain <christophe.romain@process-one.net>
|
|
|
|
%%% Purpose : Generic REST client
|
|
|
|
%%% Created : 16 Oct 2014 by Christophe Romain <christophe.romain@process-one.net>
|
|
|
|
%%%
|
|
|
|
%%%
|
2018-01-05 21:18:58 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2018 ProcessOne
|
2016-09-27 04:57:14 +02:00
|
|
|
%%%
|
|
|
|
%%% This program is free software; you can redistribute it and/or
|
|
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
|
|
%%% License, or (at your option) any later version.
|
|
|
|
%%%
|
|
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
%%% General Public License for more details.
|
|
|
|
%%%
|
|
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
%%%
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
|
|
|
-module(rest).
|
|
|
|
|
|
|
|
-behaviour(ejabberd_config).
|
|
|
|
|
|
|
|
-export([start/1, stop/1, get/2, get/3, post/4, delete/2,
|
2018-05-17 14:47:21 +02:00
|
|
|
put/4, patch/4, request/6, with_retry/4,
|
|
|
|
encode_json/1, opt_type/1]).
|
2016-09-27 04:57:14 +02:00
|
|
|
|
|
|
|
-include("logger.hrl").
|
|
|
|
|
|
|
|
-define(HTTP_TIMEOUT, 10000).
|
|
|
|
-define(CONNECT_TIMEOUT, 8000).
|
2018-05-17 14:47:21 +02:00
|
|
|
-define(CONTENT_TYPE, "application/json").
|
2016-09-27 04:57:14 +02:00
|
|
|
|
|
|
|
start(Host) ->
|
2018-04-23 17:40:44 +02:00
|
|
|
application:start(inets),
|
|
|
|
Size = ejabberd_config:get_option({ext_api_http_pool_size, Host}, 100),
|
|
|
|
httpc:set_options([{max_sessions, Size}]).
|
2016-09-27 04:57:14 +02:00
|
|
|
|
|
|
|
stop(_Host) ->
|
|
|
|
ok.
|
|
|
|
|
|
|
|
with_retry(Method, Args, MaxRetries, Backoff) ->
|
|
|
|
with_retry(Method, Args, 0, MaxRetries, Backoff).
|
|
|
|
with_retry(Method, Args, Retries, MaxRetries, Backoff) ->
|
|
|
|
case apply(?MODULE, Method, Args) of
|
|
|
|
%% Only retry on timeout errors
|
|
|
|
{error, {http_error,{error,Error}}}
|
|
|
|
when Retries < MaxRetries
|
|
|
|
andalso (Error == 'timeout' orelse Error == 'connect_timeout') ->
|
|
|
|
timer:sleep(round(math:pow(2, Retries)) * Backoff),
|
|
|
|
with_retry(Method, Args, Retries+1, MaxRetries, Backoff);
|
|
|
|
Result ->
|
|
|
|
Result
|
|
|
|
end.
|
|
|
|
|
|
|
|
get(Server, Path) ->
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, get, Path, [], ?CONTENT_TYPE, <<>>).
|
2016-09-27 04:57:14 +02:00
|
|
|
get(Server, Path, Params) ->
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, get, Path, Params, ?CONTENT_TYPE, <<>>).
|
2016-09-27 04:57:14 +02:00
|
|
|
|
|
|
|
delete(Server, Path) ->
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, delete, Path, [], ?CONTENT_TYPE, <<>>).
|
2016-09-27 04:57:14 +02:00
|
|
|
|
|
|
|
post(Server, Path, Params, Content) ->
|
2016-08-09 10:53:58 +02:00
|
|
|
Data = encode_json(Content),
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, post, Path, Params, ?CONTENT_TYPE, Data).
|
2016-09-27 04:57:14 +02:00
|
|
|
|
2016-08-09 10:53:58 +02:00
|
|
|
put(Server, Path, Params, Content) ->
|
|
|
|
Data = encode_json(Content),
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, put, Path, Params, ?CONTENT_TYPE, Data).
|
2016-08-09 10:53:58 +02:00
|
|
|
|
|
|
|
patch(Server, Path, Params, Content) ->
|
|
|
|
Data = encode_json(Content),
|
2018-05-17 14:47:21 +02:00
|
|
|
request(Server, patch, Path, Params, ?CONTENT_TYPE, Data).
|
2016-08-09 10:53:58 +02:00
|
|
|
|
2016-09-27 04:57:14 +02:00
|
|
|
request(Server, Method, Path, Params, Mime, Data) ->
|
2018-04-23 17:40:44 +02:00
|
|
|
URI = to_list(url(Server, Path, Params)),
|
2016-09-27 04:57:14 +02:00
|
|
|
Opts = [{connect_timeout, ?CONNECT_TIMEOUT},
|
|
|
|
{timeout, ?HTTP_TIMEOUT}],
|
2018-04-23 17:40:44 +02:00
|
|
|
Hdrs = [{"connection", "keep-alive"},
|
2018-05-17 14:47:21 +02:00
|
|
|
{"User-Agent", "ejabberd"}]
|
|
|
|
++ custom_headers(Server),
|
2018-04-23 17:40:44 +02:00
|
|
|
Req = if
|
|
|
|
(Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete) ->
|
|
|
|
{URI, Hdrs, to_list(Mime), Data};
|
|
|
|
true ->
|
|
|
|
{URI, Hdrs}
|
|
|
|
end,
|
2016-09-27 04:57:14 +02:00
|
|
|
Begin = os:timestamp(),
|
2018-04-23 17:40:44 +02:00
|
|
|
Result = try httpc:request(Method, Req, Opts, [{body_format, binary}]) of
|
|
|
|
{ok, {{_, Code, _}, _, <<>>}} ->
|
2016-09-27 04:57:14 +02:00
|
|
|
{ok, Code, []};
|
2018-04-23 17:40:44 +02:00
|
|
|
{ok, {{_, Code, _}, _, <<" ">>}} ->
|
2016-09-27 04:57:14 +02:00
|
|
|
{ok, Code, []};
|
2018-04-23 17:40:44 +02:00
|
|
|
{ok, {{_, Code, _}, _, <<"\r\n">>}} ->
|
2016-09-27 04:57:14 +02:00
|
|
|
{ok, Code, []};
|
2018-04-23 17:40:44 +02:00
|
|
|
{ok, {{_, Code, _}, _, Body}} ->
|
2016-09-27 04:57:14 +02:00
|
|
|
try jiffy:decode(Body) of
|
|
|
|
JSon ->
|
|
|
|
{ok, Code, JSon}
|
|
|
|
catch
|
|
|
|
_:Error ->
|
|
|
|
?ERROR_MSG("HTTP response decode failed:~n"
|
|
|
|
"** URI = ~s~n"
|
|
|
|
"** Body = ~p~n"
|
|
|
|
"** Err = ~p",
|
|
|
|
[URI, Body, Error]),
|
|
|
|
{error, {invalid_json, Body}}
|
|
|
|
end;
|
|
|
|
{error, Reason} ->
|
|
|
|
?ERROR_MSG("HTTP request failed:~n"
|
|
|
|
"** URI = ~s~n"
|
|
|
|
"** Err = ~p",
|
|
|
|
[URI, Reason]),
|
2018-04-23 17:40:44 +02:00
|
|
|
{error, {http_error, {error, Reason}}}
|
|
|
|
catch
|
|
|
|
exit:Reason ->
|
2016-09-27 04:57:14 +02:00
|
|
|
?ERROR_MSG("HTTP request failed:~n"
|
|
|
|
"** URI = ~s~n"
|
|
|
|
"** Err = ~p",
|
|
|
|
[URI, Reason]),
|
|
|
|
{error, {http_error, {error, Reason}}}
|
|
|
|
end,
|
|
|
|
ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]),
|
|
|
|
case Result of
|
|
|
|
{ok, _, _} ->
|
|
|
|
End = os:timestamp(),
|
|
|
|
Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms
|
|
|
|
ejabberd_hooks:run(backend_api_response_time, Server,
|
2018-04-18 13:16:08 +02:00
|
|
|
[Server, Method, Path, Elapsed]);
|
2016-09-27 04:57:14 +02:00
|
|
|
{error, {http_error,{error,timeout}}} ->
|
|
|
|
ejabberd_hooks:run(backend_api_timeout, Server,
|
2018-04-18 13:16:08 +02:00
|
|
|
[Server, Method, Path]);
|
2016-09-27 04:57:14 +02:00
|
|
|
{error, {http_error,{error,connect_timeout}}} ->
|
|
|
|
ejabberd_hooks:run(backend_api_timeout, Server,
|
2018-04-18 13:16:08 +02:00
|
|
|
[Server, Method, Path]);
|
2016-09-27 04:57:14 +02:00
|
|
|
{error, _} ->
|
|
|
|
ejabberd_hooks:run(backend_api_error, Server,
|
2018-04-18 13:16:08 +02:00
|
|
|
[Server, Method, Path])
|
2016-09-27 04:57:14 +02:00
|
|
|
end,
|
|
|
|
Result.
|
|
|
|
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% HTTP helpers
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
2018-04-23 17:40:44 +02:00
|
|
|
to_list(V) when is_binary(V) ->
|
|
|
|
binary_to_list(V);
|
2018-05-17 14:47:21 +02:00
|
|
|
to_list(V) when is_list(V) ->
|
2018-04-23 17:40:44 +02:00
|
|
|
V.
|
|
|
|
|
2016-08-09 10:53:58 +02:00
|
|
|
encode_json(Content) ->
|
|
|
|
case catch jiffy:encode(Content) of
|
|
|
|
{'EXIT', Reason} ->
|
|
|
|
?ERROR_MSG("HTTP content encodage failed:~n"
|
|
|
|
"** Content = ~p~n"
|
|
|
|
"** Err = ~p",
|
|
|
|
[Content, Reason]),
|
|
|
|
<<>>;
|
|
|
|
Encoded ->
|
|
|
|
Encoded
|
|
|
|
end.
|
|
|
|
|
2018-05-17 14:47:21 +02:00
|
|
|
custom_headers(Server) ->
|
|
|
|
case ejabberd_config:get_option({ext_api_headers, Server},
|
|
|
|
<<>>) of
|
|
|
|
<<>> ->
|
|
|
|
[];
|
|
|
|
Hdrs ->
|
|
|
|
lists:foldr(fun(Hdr, Acc) ->
|
|
|
|
case binary:split(Hdr, <<":">>) of
|
|
|
|
[K, V] -> [{binary_to_list(K), binary_to_list(V)}|Acc];
|
|
|
|
_ -> Acc
|
|
|
|
end
|
|
|
|
end, [], binary:split(Hdrs, <<",">>))
|
|
|
|
end.
|
|
|
|
|
2016-09-27 04:57:14 +02:00
|
|
|
base_url(Server, Path) ->
|
2018-04-18 13:16:08 +02:00
|
|
|
BPath = case iolist_to_binary(Path) of
|
2016-09-27 04:57:14 +02:00
|
|
|
<<$/, Ok/binary>> -> Ok;
|
|
|
|
Ok -> Ok
|
|
|
|
end,
|
2018-04-18 13:16:08 +02:00
|
|
|
Url = case BPath of
|
|
|
|
<<"http", _/binary>> -> BPath;
|
2016-09-27 04:57:14 +02:00
|
|
|
_ ->
|
|
|
|
Base = ejabberd_config:get_option({ext_api_url, Server},
|
|
|
|
<<"http://localhost/api">>),
|
2018-04-18 13:16:08 +02:00
|
|
|
case binary:last(Base) of
|
2018-04-23 17:40:44 +02:00
|
|
|
$/ -> <<Base/binary, BPath/binary>>;
|
2018-04-18 13:16:08 +02:00
|
|
|
_ -> <<Base/binary, "/", BPath/binary>>
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
case binary:last(Url) of
|
|
|
|
47 -> binary_part(Url, 0, size(Url)-1);
|
|
|
|
_ -> Url
|
2016-09-27 04:57:14 +02:00
|
|
|
end.
|
|
|
|
|
2018-04-18 13:16:08 +02:00
|
|
|
url(Url, []) ->
|
2018-04-23 12:29:50 +02:00
|
|
|
Url;
|
2018-04-18 13:16:08 +02:00
|
|
|
url(Url, Params) ->
|
|
|
|
L = [<<"&", (iolist_to_binary(Key))/binary, "=",
|
|
|
|
(misc:url_encode(Value))/binary>>
|
2016-09-27 04:57:14 +02:00
|
|
|
|| {Key, Value} <- Params],
|
2018-04-18 13:16:08 +02:00
|
|
|
<<$&, Encoded/binary>> = iolist_to_binary(L),
|
2018-04-23 12:29:50 +02:00
|
|
|
<<Url/binary, $?, Encoded/binary>>.
|
2018-04-18 13:16:08 +02:00
|
|
|
url(Server, Path, Params) ->
|
|
|
|
case binary:split(base_url(Server, Path), <<"?">>) of
|
|
|
|
[Url] ->
|
|
|
|
url(Url, Params);
|
|
|
|
[Url, Extra] ->
|
|
|
|
Custom = [list_to_tuple(binary:split(P, <<"=">>))
|
|
|
|
|| P <- binary:split(Extra, <<"&">>, [global])],
|
|
|
|
url(Url, Custom++Params)
|
|
|
|
end.
|
2016-09-27 04:57:14 +02:00
|
|
|
|
2018-09-09 08:59:08 +02:00
|
|
|
-spec opt_type(atom()) -> fun((any()) -> any()) | [atom()].
|
2016-09-27 04:57:14 +02:00
|
|
|
opt_type(ext_api_http_pool_size) ->
|
|
|
|
fun (X) when is_integer(X), X > 0 -> X end;
|
|
|
|
opt_type(ext_api_url) ->
|
|
|
|
fun (X) -> iolist_to_binary(X) end;
|
2018-05-17 14:47:21 +02:00
|
|
|
opt_type(ext_api_headers) ->
|
|
|
|
fun (X) -> iolist_to_binary(X) end;
|
|
|
|
opt_type(_) -> [ext_api_http_pool_size, ext_api_url, ext_api_headers].
|