--- /dev/null
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_rest.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 REST backend
+%%% Created : 26 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_rest).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1,
+ opt_type/1]).
+
+-include("ejabberd.hrl").
+-include("ejabberd_oauth.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+
+init() ->
+ rest:start(?MYNAME),
+ ok.
+
+store(R) ->
+ Path = path(<<"store">>),
+ %% Retry 2 times, with a backoff of 500millisec
+ {User, Server} = R#oauth_token.us,
+ SJID = jid:to_string({User, Server, <<"">>}),
+ case rest:with_retry(
+ post,
+ [?MYNAME, Path, [],
+ {[{<<"token">>, R#oauth_token.token},
+ {<<"user">>, SJID},
+ {<<"scope">>, R#oauth_token.scope},
+ {<<"expire">>, R#oauth_token.expire}
+ ]}], 2, 500) of
+ {ok, Code, _} when Code == 200 orelse Code == 201 ->
+ ok;
+ Err ->
+ ?ERROR_MSG("failed to store oauth record ~p: ~p", [R, Err]),
+ {error, Err}
+ end.
+
+lookup(Token) ->
+ Path = path(<<"lookup">>),
+ case rest:with_retry(post, [?MYNAME, Path, [],
+ {[{<<"token">>, Token}]}],
+ 2, 500) of
+ {ok, 200, {Data}} ->
+ SJID = proplists:get_value(<<"user">>, Data, <<>>),
+ JID = jid:from_string(SJID),
+ US = {JID#jid.luser, JID#jid.lserver},
+ Scope = proplists:get_value(<<"scope">>, Data, []),
+ Expire = proplists:get_value(<<"expire">>, Data, 0),
+ #oauth_token{token = Token,
+ us = US,
+ scope = Scope,
+ expire = Expire};
+ {ok, 404, _Resp} ->
+ false;
+ Other ->
+ ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]),
+ {error, rest_failed}
+ end.
+
+clean(_TS) ->
+ ok.
+
+path(Path) ->
+ Base = ejabberd_config:get_option(ext_api_path_oauth,
+ fun(X) -> iolist_to_binary(X) end,
+ <<"/oauth">>),
+ <<Base/binary, "/", Path/binary>>.
+
+
+opt_type(ext_api_path_oauth) ->
+ fun (X) -> iolist_to_binary(X) end;
+opt_type(_) -> [ext_api_path_oauth].
--- /dev/null
+%%%----------------------------------------------------------------------
+%%% File : http_p1.erl
+%%% Author : Emilio Bustos <ebustos@process-one.net>
+%%% Purpose : Provide a common API for inets / lhttpc / ibrowse
+%%% Created : 29 Jul 2010 by Emilio Bustos <ebustos@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% 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(http_p1).
+
+-author('ebustos@process-one.net').
+
+-export([start/0, stop/0, get/1, get/2, post/2, post/3,
+ request/3, request/4, request/5,
+ get_pool_size/0, set_pool_size/1]).
+
+-include("logger.hrl").
+
+% -define(USE_INETS, 1).
+-define(USE_LHTTPC, 1).
+% -define(USE_IBROWSE, 1).
+% inets used as default if none specified
+
+-ifdef(USE_IBROWSE).
+
+start() ->
+ ejabberd:start_app(ibrowse).
+
+stop() ->
+ application:stop(ibrowse).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ TimeOut = proplists:get_value(timeout, Opts, infinity),
+ Options = [{inactivity_timeout, TimeOut}
+ | proplists:delete(timeout, Opts)],
+ case ibrowse:send_req(URL, Hdrs, Method, Body, Options)
+ of
+ {ok, Status, Headers, Response} ->
+ {ok, jlib:binary_to_integer(Status), Headers,
+ Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ application:get_env(ibrowse, default_max_sessions, 10).
+
+set_pool_size(Size) ->
+ application:set_env(ibrowse, default_max_sessions, Size).
+
+-else.
+
+-ifdef(USE_LHTTPC).
+
+start() ->
+ ejabberd:start_app(lhttpc).
+
+stop() ->
+ application:stop(lhttpc).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ {[TO, SO], Rest} = proplists:split(Opts, [timeout, socket_options]),
+ TimeOut = proplists:get_value(timeout, TO, infinity),
+ SockOpt = proplists:get_value(socket_options, SO, []),
+ Options = [{connect_options, SockOpt} | Rest],
+ Result = lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options),
+ ?DEBUG("HTTP request -> response:~n"
+ "** Method = ~p~n"
+ "** URI = ~s~n"
+ "** Body = ~s~n"
+ "** Hdrs = ~p~n"
+ "** Timeout = ~p~n"
+ "** Options = ~p~n"
+ "** Response = ~p",
+ [Method, URL, Body, Hdrs, TimeOut, Options, Result]),
+ case Result of
+ {ok, {{Status, _Reason}, Headers, Response}} ->
+ {ok, Status, Headers, (Response)};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ Opts = proplists:get_value(lhttpc_manager, lhttpc_manager:list_pools()),
+ proplists:get_value(max_pool_size,Opts).
+
+set_pool_size(Size) ->
+ lhttpc_manager:set_max_pool_size(lhttpc_manager, Size).
+
+-else.
+
+start() ->
+ ejabberd:start_app(inets).
+
+stop() ->
+ application:stop(inets).
+
+to_list(Str) when is_binary(Str) ->
+ binary_to_list(Str);
+to_list(Str) ->
+ Str.
+
+request(Method, URLRaw, HdrsRaw, Body, Opts) ->
+ Hdrs = lists:map(fun({N, V}) ->
+ {to_list(N), to_list(V)}
+ end, HdrsRaw),
+ URL = to_list(URLRaw),
+
+ Request = case Method of
+ get -> {URL, Hdrs};
+ head -> {URL, Hdrs};
+ delete -> {URL, Hdrs};
+ _ -> % post, etc.
+ {URL, Hdrs,
+ to_list(proplists:get_value(<<"content-type">>, HdrsRaw, [])),
+ Body}
+ end,
+ Options = case proplists:get_value(timeout, Opts,
+ infinity)
+ of
+ infinity -> proplists:delete(timeout, Opts);
+ _ -> Opts
+ end,
+ case httpc:request(Method, Request, Options, []) of
+ {ok, {{_, Status, _}, Headers, Response}} ->
+ {ok, Status, Headers, Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ {ok, Size} = httpc:get_option(max_sessions),
+ Size.
+
+set_pool_size(Size) ->
+ httpc:set_option(max_sessions, Size).
+
+-endif.
+
+-endif.
+
+-type({header,
+ {type, 63, tuple,
+ [{type, 63, union,
+ [{type, 63, string, []}, {type, 63, atom, []}]},
+ {type, 63, string, []}]},
+ []}).
+
+-type({headers,
+ {type, 64, list, [{type, 64, header, []}]}, []}).
+
+-type({option,
+ {type, 67, union,
+ [{type, 67, tuple,
+ [{atom, 67, connect_timeout}, {type, 67, timeout, []}]},
+ {type, 68, tuple,
+ [{atom, 68, timeout}, {type, 68, timeout, []}]},
+ {type, 70, tuple,
+ [{atom, 70, send_retry},
+ {type, 70, non_neg_integer, []}]},
+ {type, 71, tuple,
+ [{atom, 71, partial_upload},
+ {type, 71, union,
+ [{type, 71, non_neg_integer, []},
+ {atom, 71, infinity}]}]},
+ {type, 72, tuple,
+ [{atom, 72, partial_download}, {type, 72, pid, []},
+ {type, 72, union,
+ [{type, 72, non_neg_integer, []},
+ {atom, 72, infinity}]}]}]},
+ []}).
+
+-type({options,
+ {type, 74, list, [{type, 74, option, []}]}, []}).
+
+-type({result,
+ {type, 76, union,
+ [{type, 76, tuple,
+ [{atom, 76, ok},
+ {type, 76, tuple,
+ [{type, 76, tuple,
+ [{type, 76, pos_integer, []}, {type, 76, string, []}]},
+ {type, 76, headers, []}, {type, 76, string, []}]}]},
+ {type, 77, tuple,
+ [{atom, 77, error}, {type, 77, atom, []}]}]},
+ []}).
+
+%% @spec (URL) -> Result
+%% URL = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, [])',
+%% that is {@link request/3} with an empty header list.
+%% @end
+%% @see request/3
+-spec get(string()) -> result().
+get(URL) -> request(get, URL, []).
+
+%% @spec (URL, Hdrs) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, Hdrs)'.
+%% @end
+%% @see request/3
+-spec get(string(), headers()) -> result().
+get(URL, Hdrs) -> request(get, URL, Hdrs).
+
+%% @spec (URL, RequestBody) -> Result
+%% URL = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request with form data.
+%% Would be the same as calling
+%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), string()) -> result().
+post(URL, Body) ->
+ request(post, URL,
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}],
+ Body).
+
+%% @spec (URL, Hdrs, RequestBody) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request.
+%% Would be the same as calling
+%% `request(post, URL, Hdrs, Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), headers(), string()) -> result().
+post(URL, Hdrs, Body) ->
+ NewHdrs = case [X
+ || {X, _} <- Hdrs,
+ str:to_lower(X) == <<"content-type">>]
+ of
+ [] ->
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}
+ | Hdrs];
+ _ -> Hdrs
+ end,
+ request(post, URL, NewHdrs, Body).
+
+%% @spec (Method, URL, Hdrs) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request without a body.
+%% Would be the same as calling `request(Method, URL, Hdrs, [], [])',
+%% that is {@link request/5} with an empty body.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers()) -> result().
+request(Method, URL, Hdrs) ->
+ request(Method, URL, Hdrs, [], []).
+
+%% @spec (Method, URL, Hdrs, RequestBody) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string()) -> result().
+request(Method, URL, Hdrs, Body) ->
+ request(Method, URL, Hdrs, Body, []).
+
+%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Options = [Option]
+%% Option = {timeout, Milliseconds | infinity} |
+%% {connect_timeout, Milliseconds | infinity} |
+%% {socket_options, [term()]} |
+
+%% Milliseconds = integer()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string(), options()) -> result().
+
+% ibrowse {response_format, response_format()} |
+% Options - [option()]
+% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result,
+% boolean()} | {headers_as_is, boolean()}
+%body_format() = string() | binary()
+% The body_format option is only valid for the synchronous request and the default is string.
+% When making an asynchronous request the body will always be received as a binary.
+% lhttpc: always binary
+
--- /dev/null
+%%%----------------------------------------------------------------------
+%%% 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>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% 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,
+ request/6, with_retry/4, opt_type/1]).
+
+-include("logger.hrl").
+
+-define(HTTP_TIMEOUT, 10000).
+-define(CONNECT_TIMEOUT, 8000).
+
+start(Host) ->
+ http_p1:start(),
+ Pool_size =
+ ejabberd_config:get_option({ext_api_http_pool_size, Host},
+ fun(X) when is_integer(X), X > 0->
+ X
+ end,
+ 100),
+ http_p1:set_pool_size(Pool_size).
+
+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) ->
+ request(Server, get, Path, [], "application/json", <<>>).
+get(Server, Path, Params) ->
+ request(Server, get, Path, Params, "application/json", <<>>).
+
+delete(Server, Path) ->
+ request(Server, delete, Path, [], "application/json", <<>>).
+
+post(Server, Path, Params, Content) ->
+ Data = 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,
+ request(Server, post, Path, Params, "application/json", Data).
+
+request(Server, Method, Path, Params, Mime, Data) ->
+ URI = url(Server, Path, Params),
+ Opts = [{connect_timeout, ?CONNECT_TIMEOUT},
+ {timeout, ?HTTP_TIMEOUT}],
+ Hdrs = [{"connection", "keep-alive"},
+ {"content-type", Mime},
+ {"User-Agent", "ejabberd"}],
+ Begin = os:timestamp(),
+ Result = case catch http_p1:request(Method, URI, Hdrs, Data, Opts) of
+ {ok, Code, _, <<>>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<" ">>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<"\r\n">>} ->
+ {ok, Code, []};
+ {ok, Code, _, Body} ->
+ 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]),
+ {error, {http_error, {error, Reason}}};
+ {'EXIT', Reason} ->
+ ?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,
+ [Server, Method, Path, Elapsed]);
+ {error, {http_error,{error,timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, {http_error,{error,connect_timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, _} ->
+ ejabberd_hooks:run(backend_api_error, Server,
+ [Server, Method, Path])
+ end,
+ Result.
+
+%%%----------------------------------------------------------------------
+%%% HTTP helpers
+%%%----------------------------------------------------------------------
+
+base_url(Server, Path) ->
+ Tail = case iolist_to_binary(Path) of
+ <<$/, Ok/binary>> -> Ok;
+ Ok -> Ok
+ end,
+ case Tail of
+ <<"http", _Url/binary>> -> Tail;
+ _ ->
+ Base = ejabberd_config:get_option({ext_api_url, Server},
+ fun(X) ->
+ iolist_to_binary(X)
+ end,
+ <<"http://localhost/api">>),
+ <<Base/binary, "/", Tail/binary>>
+ end.
+
+url(Server, Path, []) ->
+ binary_to_list(base_url(Server, Path));
+url(Server, Path, Params) ->
+ Base = base_url(Server, Path),
+ [<<$&, ParHead/binary>> | ParTail] =
+ [<<"&", (iolist_to_binary(Key))/binary, "=",
+ (ejabberd_http:url_encode(Value))/binary>>
+ || {Key, Value} <- Params],
+ Tail = iolist_to_binary([ParHead | ParTail]),
+ binary_to_list(<<Base/binary, $?, Tail/binary>>).
+
+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;
+opt_type(_) -> [ext_api_http_pool_size, ext_api_url].