]> granicus.if.org Git - ejabberd/commitdiff
Add http_p1.erl, rest.erl, and oauth2 ReST backend for tokens.
authorAlexey Shchepin <alexey@process-one.net>
Tue, 27 Sep 2016 02:57:14 +0000 (05:57 +0300)
committerAlexey Shchepin <alexey@process-one.net>
Tue, 27 Sep 2016 02:57:14 +0000 (05:57 +0300)
src/ejabberd_app.erl
src/ejabberd_oauth_rest.erl [new file with mode: 0644]
src/http_p1.erl [new file with mode: 0644]
src/rest.erl [new file with mode: 0644]

index 890ab6f9044ea4d8613bbe431416f3e92c10d8ae..33da450135b0657a0e44cefcd3d722b757172597 100644 (file)
@@ -225,6 +225,7 @@ start_apps() ->
     ejabberd:start_app(fast_tls),
     ejabberd:start_app(fast_xml),
     ejabberd:start_app(stringprep),
+    http_p1:start(),
     ejabberd:start_app(cache_tab).
 
 opt_type(net_ticktime) ->
diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl
new file mode 100644 (file)
index 0000000..aadb970
--- /dev/null
@@ -0,0 +1,98 @@
+%%%-------------------------------------------------------------------
+%%% 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].
diff --git a/src/http_p1.erl b/src/http_p1.erl
new file mode 100644 (file)
index 0000000..6ede758
--- /dev/null
@@ -0,0 +1,358 @@
+%%%----------------------------------------------------------------------
+%%% 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
+
diff --git a/src/rest.erl b/src/rest.erl
new file mode 100644 (file)
index 0000000..01b04f6
--- /dev/null
@@ -0,0 +1,181 @@
+%%%----------------------------------------------------------------------
+%%% 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].