This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to branch iam-auth-couch_gun
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit c6255bb484d70a115e90a27b95f7674ff4217173
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Thu Sep 3 12:36:43 2026 -0400

    Use couch_gun for auth ibm
    
    Copied couch_gun from https://github.com/apache/couchdb/pull/6098
    
    Don't bother starting a pool as we'd keep the connection open for an hour 
and
    at those time intervals it doesn't matter that much
---
 src/couch/src/couch_gun.erl                        | 439 +++++++++++++++++++++
 .../src/couch_replicator_auth_ibm.erl              | 186 +++------
 2 files changed, 496 insertions(+), 129 deletions(-)

diff --git a/src/couch/src/couch_gun.erl b/src/couch/src/couch_gun.erl
new file mode 100644
index 000000000..e207283ce
--- /dev/null
+++ b/src/couch/src/couch_gun.erl
@@ -0,0 +1,439 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% Helper module to use gun instead of other http clients we had.
+%
+% Functions:
+%
+%   * req/3,4,5: For basic synchronous requests
+%
+%   * parse_url/1: Helper parser to turn urls with possible auth bits
+%     embedded into a gun uri map
+%
+%   * open/3, close/1: Open/close connections
+%
+%   * send/5,6: Send requests on opened connections. Can take {Fun, State} for
+%     a streaming body.
+%
+%   * await/3: Wait for response a send.
+%
+%   * headers/1, method/1, basic_auth/2: convert existing calling conventions
+%     to gun's format. These are helpers to avoid modifying all the call sites
+%     right off the bat. For example, the codebase expects headers to be
+%     strings and methods to be atom so we transform them here accordingly.
+%
+%   * norm_error/1: shorten/normalize gun's error reason
+
+-module(couch_gun).
+
+-export([
+    req/3,
+    req/4,
+    req/5,
+    parse_url/1,
+    open/3,
+    close/1,
+    send/5,
+    send/6,
+    await/3,
+    headers/1,
+    method/1,
+    basic_auth/2,
+    norm_error/1
+]).
+
+-define(DEFAULT_TIMEOUT, 30000).
+
+req(Method, Url, Headers) ->
+    req(Method, Url, Headers, <<>>, #{}).
+
+req(Method, Url, Headers, Body) ->
+    req(Method, Url, Headers, Body, #{}).
+
+req(Method, Url, Headers, Body, #{} = Opts) when is_atom(Method), is_list(Url) 
->
+    case parse_url(Url) of
+        {ok, #{transport := Transport, host := Host, port := Port} = Parsed} ->
+            #{path := Path, userinfo := UserInfo} = Parsed,
+            Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
+            Headers1 = auth_headers(headers(Headers), UserInfo, Opts),
+            OpenOpts = maps:with([tls_opts, tcp_opts, gun_opts], Opts),
+            case open(Host, Port, OpenOpts#{transport => Transport}) of
+                {ok, Pid} ->
+                    try
+                        case gun:await_up(Pid, Timeout) of
+                            {ok, _} ->
+                                Ref = send(Pid, Method, Path, Headers1, Body),
+                                await(Pid, Ref, Timeout);
+                            {error, Reason} ->
+                                {error, norm_error(Reason)}
+                        end
+                    after
+                        close(Pid)
+                    end;
+                {error, Reason} ->
+                    {error, norm_error(Reason)}
+            end;
+        {error, _} = Error ->
+            Error
+    end.
+
+parse_url("http://"; ++ Rest) ->
+    parse_auth(tcp, 80, Rest);
+parse_url("https://"; ++ Rest) ->
+    parse_auth(tls, 443, Rest);
+parse_url(_) ->
+    {error, invalid_uri}.
+
+parse_auth(Transport, DefaultPort, Rest) ->
+    {Auth, Path} =
+        case lists:splitwith(fun(C) -> C /= $/ andalso C /= $? end, Rest) of
+            {A, ""} -> {A, "/"};
+            {A, "?" ++ _ = Query} -> {A, "/" ++ Query};
+            {A, P} -> {A, P}
+        end,
+    {UserInfo, HostPort} =
+        case string:split(Auth, "@", trailing) of
+            [Creds, HP] -> {Creds, HP};
+            [HP] -> {undefined, HP}
+        end,
+    case parse_host_port(HostPort, DefaultPort) of
+        {error, _} = Error ->
+            Error;
+        {Host, Port} ->
+            {ok, #{
+                transport => Transport,
+                host => Host,
+                port => Port,
+                path => Path,
+                userinfo => UserInfo
+            }}
+    end.
+
+parse_host_port("", _DefaultPort) ->
+    {error, invalid_uri};
+parse_host_port("[" ++ Rest, DefaultPort) ->
+    % ipv6 with brackets [...]
+    case string:split(Rest, "]") of
+        ["", _] ->
+            {error, invalid_uri};
+        [Host, ""] ->
+            {Host, DefaultPort};
+        [Host, ":" ++ PortStr] ->
+            case string:to_integer(PortStr) of
+                {Port, ""} when is_integer(Port) -> {Host, Port};
+                _ -> {error, invalid_uri}
+            end;
+        _ ->
+            {error, invalid_uri}
+    end;
+parse_host_port(HostPort, DefaultPort) ->
+    case string:split(HostPort, ":", trailing) of
+        ["", _] ->
+            {error, invalid_uri};
+        [Host, PortStr] ->
+            case string:to_integer(PortStr) of
+                {Port, ""} when is_integer(Port) -> {Host, Port};
+                _ -> {error, invalid_uri}
+            end;
+        [Host] ->
+            {Host, DefaultPort}
+    end.
+
+% Spawn an http/1.1 gun connection process. Opts is a map that can have
+% these fields:
+%
+%   transport - tcp | tls (default is tcp)
+%   tls_opts  - tls client opts, default is []
+%   tcp_opts  - gen_tcp options
+%   gun_opts  - Other gun options (see gun:open/3 docs)
+%
+% Host could be a string or address tuple. We don't wait for the connection to
+% be up a caller may do that with gun:wait_up/2 to get connection errors
+% earlier than during the first send.
+%
+open(Host, Port, #{} = Opts) ->
+    Transport = maps:get(transport, Opts, tcp),
+    OpenOpts0 = #{transport => Transport, protocols => [http], retry => 0},
+    OpenOpts1 =
+        case Opts of
+            #{tcp_opts := TcpOpts} -> OpenOpts0#{tcp_opts => TcpOpts};
+            #{} -> OpenOpts0
+        end,
+    OpenOpts2 =
+        case Transport of
+            tls -> OpenOpts1#{tls_opts => maps:get(tls_opts, Opts, [])};
+            tcp -> OpenOpts1
+        end,
+    OpenOpts = maps:merge(OpenOpts2, maps:get(gun_opts, Opts, #{})),
+    gun:open(host(Host), Port, OpenOpts).
+
+close(Pid) when is_pid(Pid) ->
+    try
+        gun:close(Pid)
+    catch
+        _:_ -> ok
+    end,
+    ok.
+
+% Send a request and get back a stream ref. Body may be a {Fun, State} tuple.
+% Then Fun(State) should return {ok, Data, NewState} and then return eof at the
+% end. Data will be sent chunked unless a content-length header is set.
+send(Pid, Method, Path, Headers, Body) ->
+    send(Pid, Method, Path, Headers, Body, #{}).
+
+send(Pid, Method, Path, Headers, {Fun, State}, ReqOpts) when is_function(Fun, 
1) ->
+    SRef = gun:headers(Pid, method(Method), Path, headers(Headers), ReqOpts),
+    ok = send_body(Pid, SRef, Fun, State),
+    SRef;
+send(Pid, Method, Path, Headers, Body, ReqOpts) when is_pid(Pid) ->
+    gun:request(Pid, method(Method), Path, headers(Headers), body(Body), 
ReqOpts).
+
+body([]) ->
+    <<>>;
+body(Body) ->
+    Body.
+
+send_body(Pid, SRef, Fun, State) ->
+    case Fun(State) of
+        {ok, Data, State1} ->
+            send_body(Pid, SRef, Fun, State1, Data);
+        eof ->
+            ok = gun:data(Pid, SRef, fin, <<>>)
+    end.
+
+send_body(Pid, SRef, Fun, State, Data0) ->
+    case Fun(State) of
+        {ok, Data, State1} ->
+            % Send pending data before sending the next.
+            % We're doing one chunk at a time here
+            ok = gun:data(Pid, SRef, nofin, Data0),
+            send_body(Pid, SRef, Fun, State1, Data);
+        eof ->
+            ok = gun:data(Pid, SRef, fin, Data0)
+    end.
+
+% Wait for a response. First wait for status + headers then body. 1xx info
+% responses are skipped and we don't care about trailers either. If we got a
+% bad connection and didn't find out until calling send and await we'll get the
+% error here.
+await(Pid, SRef, Timeout) when is_pid(Pid) ->
+    MRef = monitor(process, Pid),
+    try await_headers(Pid, SRef, Timeout, MRef) of
+        {response, fin, Code, RespHeaders} ->
+            {ok, Code, RespHeaders, <<>>};
+        {response, nofin, Code, RespHeaders} ->
+            case gun:await_body(Pid, SRef, Timeout, MRef) of
+                {ok, RespBody} -> {ok, Code, RespHeaders, RespBody};
+                {ok, RespBody, _Trailers} -> {ok, Code, RespHeaders, RespBody};
+                {error, Reason} -> {error, norm_error(Reason)}
+            end;
+        {error, Reason} ->
+            {error, norm_error(Reason)}
+    after
+        demonitor(MRef, [flush])
+    end.
+
+await_headers(Pid, SRef, Timeout, MRef) ->
+    case gun:await(Pid, SRef, Timeout, MRef) of
+        {inform, _Status, _Headers} -> await_headers(Pid, SRef, Timeout, MRef);
+        Other -> Other
+    end.
+
+% Transform our request headers into gun's lowercase binary shape. Previous
+% http client accepted special atom headers like {basic_auth, {User, Pass}} and
+% {cookie, Value}, {content_type, Type}, {content_length, Len}. We handle those
+% here to avoid modifying all the call sites.
+
+headers(Headers) ->
+    [header(H) || H <- Headers].
+
+header({basic_auth, {User, Pass}}) ->
+    basic_auth(User, Pass);
+header({cookie, Cookie}) ->
+    {~"cookie", to_bin(Cookie)};
+header({content_type, Value}) ->
+    {~"content-type", to_bin(Value)};
+header({content_length, Value}) ->
+    {~"content-length", to_bin(Value)};
+header({Name, Value}) ->
+    {string:lowercase(to_bin(Name)), to_bin(Value)}.
+
+method(Method) when is_atom(Method) ->
+    string:uppercase(atom_to_binary(Method, utf8)).
+
+basic_auth(User, Pass) ->
+    UserPass = base64:encode(iolist_to_binary([User, $:, Pass])),
+    {~"authorization", <<"Basic ", UserPass/binary>>}.
+
+% If headers already have authorization set use that, otherwise take from the
+% userinfo field from the url
+auth_headers(Headers, UserInfo, Opts) ->
+    case lists:keymember(~"authorization", 1, Headers) of
+        true ->
+            Headers;
+        false ->
+            case {Opts, UserInfo} of
+                {#{basic_auth := {User, Pass}}, _} ->
+                    [basic_auth(User, Pass) | Headers];
+                {#{}, undefined} ->
+                    Headers;
+                {#{}, UserInfo} ->
+                    case string:split(UserInfo, ":") of
+                        [User, Pass] -> [basic_auth(User, Pass) | Headers];
+                        [User] -> [basic_auth(User, "") | Headers]
+                    end
+            end
+    end.
+
+to_bin(V) when is_atom(V) ->
+    atom_to_binary(V, utf8);
+to_bin(V) ->
+    iolist_to_binary(V).
+
+% gun expects IPs as address tuples
+host(Host) when is_list(Host) ->
+    case inet:parse_strict_address(Host) of
+        {ok, Ip} -> Ip;
+        {error, _} -> Host
+    end;
+host(Host) ->
+    Host.
+
+% To simplify error handling return {error, Reason} to make it easier for
+% callers to handle it instead of the multi-level nested error shapes from gun.
+norm_error({stream_error, Reason}) ->
+    norm_error(Reason);
+norm_error({connection_error, Reason}) ->
+    norm_error(Reason);
+norm_error({down, {shutdown, Reason}}) ->
+    norm_error(Reason);
+norm_error({down, Reason}) ->
+    norm_error(Reason);
+norm_error({shutdown, Reason}) ->
+    norm_error(Reason);
+norm_error(Reason) ->
+    Reason.
+
+-ifdef(TEST).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+parse_url_test() ->
+    ?assertEqual(
+        {ok, #{transport => tcp, host => "h", port => 80, path => "/", 
userinfo => undefined}},
+        parse_url("http://h";)
+    ),
+    ?assertEqual(
+        {ok, #{
+            transport => tcp, host => "h", port => 5984, path => "/db?a=b", 
userinfo => undefined
+        }},
+        parse_url("http://h:5984/db?a=b";)
+    ),
+    ?assertEqual(
+        {ok, #{transport => tls, host => "h", port => 443, path => "/", 
userinfo => undefined}},
+        parse_url("https://h";)
+    ),
+    ?assertEqual(
+        {ok, #{
+            transport => tcp, host => "127.0.0.1", port => 80, path => "/", 
userinfo => undefined
+        }},
+        parse_url("http://127.0.0.1";)
+    ),
+    ?assertEqual(
+        {ok, #{transport => tcp, host => "::1", port => 5984, path => "/db", 
userinfo => undefined}},
+        parse_url("http://[::1]:5984/db";)
+    ),
+    ?assertEqual(
+        {ok, #{transport => tcp, host => "::1", port => 80, path => "/", 
userinfo => undefined}},
+        parse_url("http://[::1]";)
+    ),
+    ?assertEqual(
+        {ok, #{transport => tcp, host => "h", port => 80, path => "/", 
userinfo => "u:p"}},
+        parse_url("http://u:p@h";)
+    ),
+    ?assertEqual(
+        {ok, #{
+            transport => tcp,
+            host => "h",
+            port => 15984,
+            path => "/_dbs_info?startkey=\"db1\"&endkey=\"db2\"",
+            userinfo => undefined
+        }},
+        parse_url("http://h:15984/_dbs_info?startkey=\"db1\"&endkey=\"db2\"";)
+    ),
+    ?assertEqual(
+        {ok, #{transport => tcp, host => "h", port => 80, path => "/?q=1", 
userinfo => undefined}},
+        parse_url("http://h?q=1";)
+    ),
+    ?assertEqual({error, invalid_uri}, parse_url("a potato")),
+    ?assertEqual({error, invalid_uri}, parse_url("ftp://h/";)),
+    ?assertEqual({error, invalid_uri}, parse_url("http://";)),
+    ?assertEqual({error, invalid_uri}, parse_url("http://:80/";)),
+    ?assertEqual({error, invalid_uri}, parse_url("http://h:x/";)),
+    ?assertEqual({error, invalid_uri}, parse_url("http://[::1";)),
+    ?assertEqual({error, invalid_uri}, parse_url("http://[]:80/";)).
+
+host_test() ->
+    ?assertEqual("cdb.example.com", host("cdb.example.com")),
+    ?assertEqual({127, 0, 0, 1}, host("127.0.0.1")),
+    ?assertEqual({0, 0, 0, 0, 0, 0, 0, 1}, host("::1")),
+    ?assertEqual({1, 2, 3, 4}, host({1, 2, 3, 4})).
+
+headers_test() ->
+    ?assertEqual([], headers([])),
+    ?assertEqual(
+        [{~"content-type", ~"application/json"}],
+        headers([{"Content-Type", "application/json"}])
+    ),
+    ?assertEqual(
+        [{~"x-foo", ~"1"}, {~"accept", ~"*/*"}],
+        headers([{'X-Foo', "1"}, {~"Accept", ~"*/*"}])
+    ),
+    ?assertEqual(
+        [basic_auth("u", "p"), {~"cookie", ~"k=v"}],
+        headers([{basic_auth, {"u", "p"}}, {cookie, "k=v"}])
+    ),
+    ?assertEqual(
+        [{~"content-type", ~"text/plain"}, {~"content-length", ~"3"}],
+        headers([{content_type, "text/plain"}, {content_length, "3"}])
+    ).
+
+method_test() ->
+    ?assertEqual(~"GET", method(get)),
+    ?assertEqual(~"COPY", method(copy)),
+    ?assertEqual(~"DELETE", method('Delete')).
+
+basic_auth_test() ->
+    ?assertEqual(
+        {~"authorization", <<"Basic ", (base64:encode(~"u:p"))/binary>>},
+        basic_auth("u", "p")
+    ),
+    ?assertEqual(basic_auth("u", "p"), basic_auth(~"u", ~"p")).
+
+auth_headers_test() ->
+    Auth = basic_auth("u", "p"),
+    Override = [{~"authorization", ~"Bearer dabears"}],
+    ?assertEqual([], auth_headers([], undefined, #{})),
+    ?assertEqual([Auth], auth_headers([], undefined, #{basic_auth => {"u", 
"p"}})),
+    ?assertEqual([Auth], auth_headers([], "u:p", #{})),
+    ?assertEqual([basic_auth("u", "")], auth_headers([], "u", #{})),
+    ?assertEqual([Auth], auth_headers([], "x:y", #{basic_auth => {"u", "p"}})),
+    ?assertEqual(Override, auth_headers(Override, "x:y", #{basic_auth => {"u", 
"p"}})).
+
+norm_error_test() ->
+    ?assertEqual(econnrefused, norm_error({down, {shutdown, econnrefused}})),
+    ?assertEqual(closed, norm_error({stream_error, closed})),
+    ?assertEqual(closed, norm_error({connection_error, closed})),
+    ?assertEqual(timeout, norm_error(timeout)),
+    ?assertEqual(normal, norm_error({down, normal})).
+
+-endif.
diff --git a/src/couch_replicator/src/couch_replicator_auth_ibm.erl 
b/src/couch_replicator/src/couch_replicator_auth_ibm.erl
index 40f66e7c4..5f3a84b91 100644
--- a/src/couch_replicator/src/couch_replicator_auth_ibm.erl
+++ b/src/couch_replicator/src/couch_replicator_auth_ibm.erl
@@ -67,7 +67,7 @@
 
 -record(state, {
     mac_key,
-    token_uri_map
+    token_url
 }).
 
 -record(public_entry, {
@@ -154,17 +154,17 @@ update_last_used(#worker_state{} = WorkerState) ->
 %% gen_server callbacks.
 
 init(_) ->
-    case token_uri_map() of
-        {ok, TokenURIMap} ->
+    case token_url() of
+        {ok, TokenUrl} ->
             ?PUBLIC = ets:new(?PUBLIC, [protected, {keypos, 
#public_entry.api_key_mac}, named_table]),
             ?PRIVATE = ets:new(?PRIVATE, [
                 private, {keypos, #private_entry.api_key_mac}, named_table
             ]),
             ok = config:listen_for_changes(?MODULE, nil),
-            start_gun(#state{
+            {ok, #state{
                 mac_key = crypto:strong_rand_bytes(32),
-                token_uri_map = TokenURIMap
-            });
+                token_url = TokenUrl
+            }};
         {error, Reason} ->
             {error, Reason}
     end.
@@ -249,14 +249,6 @@ handle_info({expire_api_key_entry, APIKeyMAC}, State) ->
             reply_all(Entry, {error, expired_api_key_entry})
     end,
     {noreply, State};
-handle_info({'DOWN', AcquireMRef, process, _AcquirePid, {error, Reason}}, 
#state{} = State) ->
-    case match_on_acquire_mref(AcquireMRef) of
-        [#private_entry{} = Entry] ->
-            reply_and_reset(Entry, {error, Reason});
-        [] ->
-            ok
-    end,
-    {noreply, State};
 handle_info(
     {'DOWN', AcquireMRef, process, _AcquirePid, {ok, StatusCode, 
_ResponseHeaders, ResponseBody}},
     #state{} = State
@@ -327,28 +319,32 @@ handle_info(
             ok
     end,
     {noreply, State};
+handle_info({'DOWN', AcquireMRef, process, _AcquirePid, Reason}, #state{} = 
State) ->
+    %% acquire process exited with {error, Reason} or died some other way
+    %% we don't care how, we always clean up after it
+    case match_on_acquire_mref(AcquireMRef) of
+        [#private_entry{} = Entry] ->
+            reply_and_reset(Entry, acquire_error(Reason));
+        [] ->
+            ok
+    end,
+    {noreply, State};
 handle_info(restart_config_listener, State) ->
     ok = config:listen_for_changes(?MODULE, nil),
     {noreply, State};
-handle_info(token_url_change, State0) ->
-    case token_uri_map() of
-        {ok, TokenURIMap} ->
-            stop_gun(State0),
-            case start_gun(State0#state{token_uri_map = TokenURIMap}) of
-                {ok, State1} ->
-                    {noreply, State1};
-                {error, Reason} ->
-                    {stop, Reason, State0}
-            end;
+handle_info(token_url_change, State) ->
+    case token_url() of
+        {ok, TokenUrl} ->
+            %% picked up by the next request, nothing to reconnect
+            {noreply, State#state{token_url = TokenUrl}};
         {error, Reason} ->
-            {stop, Reason, State0}
+            {stop, Reason, State}
     end;
 handle_info(Msg, State) ->
     couch_log:warning("~p: unexpected info ~p", [?MODULE, Msg]),
     {noreply, State}.
 
-terminate(_Reason, State) ->
-    stop_gun(State),
+terminate(_Reason, _State) ->
     ets:foldl(
         fun(#private_entry{} = Entry, Acc) ->
             cancel_timer(Entry#private_entry.refresh_ref),
@@ -394,65 +390,33 @@ extract_api_key(#httpdb{auth_props = AuthProps}) ->
             {error, missing_api_key}
     end.
 
-acquire_token(APIKey, #state{} = State) ->
+acquire_token(APIKey, #state{token_url = Url}) ->
     Headers = [
-        {~"content-type", ~"application/x-www-form-urlencoded"},
-        host_header(State)
+        {~"content-type", ~"application/x-www-form-urlencoded"}
     ],
     Body = mochiweb_util:urlencode([
         {~"grant_type", ~"urn:ibm:params:oauth:grant-type:apikey"},
         {~"response_type", ~"cloud_iam"},
         {~"apikey", APIKey}
     ]),
-    #{path := Path} = State#state.token_uri_map,
-    {_Pid, Ref} = spawn_monitor(
-        post(fix_path(Path), Headers, Body)
-    ),
+    Timeout = config:get_integer("ibm", "request_timeout", 30000),
+    {_Pid, Ref} = spawn_monitor(fun() ->
+        exit(
+            try
+                post(Url, Headers, Body, Timeout)
+            catch
+                Tag:Err -> {error, {Tag, Err}}
+            end
+        )
+    end),
     Ref.
 
-post(Path, ReqHeaders, ReqBody) ->
-    post(Path, ReqHeaders, ReqBody, 5).
-
-post(Path, ReqHeaders, ReqBody, RemainingTries) ->
-    fun() ->
-        case gun_pool:post(Path, ReqHeaders, ReqBody) of
-            {async, PoolStreamRef} ->
-                Timeout = config:get_integer("ibm", "request_timeout", 30000),
-                case gun_pool:await(PoolStreamRef, Timeout) of
-                    {response, fin, StatusCode, _RespHeaders} ->
-                        exit({error, {unexpected_status_code, StatusCode}});
-                    {response, nofin, StatusCode, RespHeaders} ->
-                        case gun_pool:await_body(PoolStreamRef, Timeout) of
-                            {ok, RespBody} ->
-                                exit({ok, StatusCode, RespHeaders, RespBody});
-                            {error, Reason} ->
-                                exit({error, Reason})
-                        end
-                end;
-            {error, no_connection_available, _Reason} when RemainingTries > 0 
->
-                timer:sleep(1000),
-                post(Path, ReqHeaders, ReqBody, RemainingTries - 1);
-            {error, _Type, Reason} ->
-                exit({error, Reason})
-        end
-    end.
-
-host_header(#state{} = State) ->
-    #{host := Host} = State#state.token_uri_map,
-    Port = maps:get(port, State#state.token_uri_map, undefined),
-    case Port of
-        undefined ->
-            {~"host", Host};
-        _ when is_integer(Port) ->
-            {~"host", [Host, $:, integer_to_list(Port)]}
-    end.
-
-fix_path([]) ->
-    ~"/";
-fix_path(Path) when is_binary(Path), byte_size(Path) > 0 ->
-    Path;
-fix_path(Path) when is_list(Path), length(Path) > 0 ->
-    Path.
+post(Url, ReqHeaders, ReqBody, Timeout) ->
+    Opts = #{
+        timeout => Timeout,
+        tls_opts => [{cacerts, couch_replicator_utils:cacert_get()}]
+    },
+    couch_gun:req(post, Url, ReqHeaders, ReqBody, Opts).
 
 decode_iam_response(ResponseBody) ->
     try jiffy:decode(ResponseBody, [return_maps]) of
@@ -501,13 +465,13 @@ api_key_uuid(EncodedToken) when is_binary(EncodedToken) ->
             ~""
     end.
 
--spec token_uri_map() -> {ok, uri_string:uri_map()} | {error, term()}.
-token_uri_map() ->
-    URI = config:get("ibm", "token_url", 
"https://iam.cloud.ibm.com/identity/token";),
-    case uri_string:parse(URI) of
-        #{} = URIMap ->
-            {ok, URIMap};
-        {error, Reason, _Details} ->
+-spec token_url() -> {ok, string()} | {error, term()}.
+token_url() ->
+    Url = config:get("ibm", "token_url", 
"https://iam.cloud.ibm.com/identity/token";),
+    case couch_gun:parse_url(Url) of
+        {ok, #{}} ->
+            {ok, Url};
+        {error, Reason} ->
             {error, Reason}
     end.
 
@@ -517,6 +481,11 @@ token_timeout() ->
 match_on_acquire_mref(AcquireMRef) ->
     ets:match_object(?PRIVATE, #private_entry{acquire_mref = AcquireMRef, _ = 
'_'}).
 
+acquire_error({error, _} = Error) ->
+    Error;
+acquire_error(Reason) ->
+    {error, Reason}.
+
 -spec reply_and_reset(Entry :: #private_entry{}, Reply :: term()) -> ok.
 reply_and_reset(#private_entry{} = Entry, Reply) ->
     ets:insert(?PRIVATE, Entry#private_entry{
@@ -537,35 +506,6 @@ cancel_timer(undefined) ->
 cancel_timer(TimerRef) when is_reference(TimerRef) ->
     erlang:cancel_timer(TimerRef).
 
-start_gun(#state{} = State) ->
-    #{host := Host, scheme := Scheme} = State#state.token_uri_map,
-    Port = port(State#state.token_uri_map),
-    ConnOptions = #{
-        transport => transport(Scheme), tls_opts => [{cacerts, 
couch_replicator_utils:cacert_get()}]
-    },
-    case gun_pool:start_pool(Host, Port, #{size => 5, conn_opts => 
ConnOptions}) of
-        {ok, PoolPid} ->
-            gun_pool:await_up(PoolPid),
-            {ok, State};
-        {error, Reason} ->
-            {error, Reason}
-    end.
-
-stop_gun(#state{} = State) ->
-    #{host := Host, scheme := Scheme} = State#state.token_uri_map,
-    Port = port(State#state.token_uri_map),
-    gun_pool:stop_pool(Host, Port, #{transport => transport(Scheme)}).
-
-port(#{port := Port}) ->
-    Port;
-port(#{scheme := "https"}) ->
-    443.
-
-transport("http") ->
-    tcp;
-transport("https") ->
-    tls.
-
 mac(Key, Data) when is_binary(Key), is_binary(Data) ->
     crypto:mac(hmac, sha256, Key, Data).
 
@@ -710,24 +650,12 @@ handle_response_test() ->
 cleanup_test() ->
     ?assertEqual(ok, cleanup(#worker_state{})).
 
-port_test() ->
-    ?assertEqual(1080, port(#{port => 1080})),
-    ?assertEqual(443, port(#{scheme => "https"})),
-    ?assertError(function_clause, port(#{})).
-
-token_uri_map_test() ->
+token_url_test() ->
     try
         meck:expect(config, get, fun("ibm", "token_url", _) -> 
"https://nohost.invalid"; end),
-        ?assertMatch(
-            {ok, #{
-                scheme := "https",
-                path := [],
-                host := "nohost.invalid"
-            }},
-            token_uri_map()
-        ),
-        meck:expect(config, get, fun("ibm", "token_url", _) -> "https:// " 
end),
-        ?assertEqual({error, invalid_uri}, token_uri_map())
+        ?assertEqual({ok, "https://nohost.invalid"}, token_url()),
+        meck:expect(config, get, fun("ibm", "token_url", _) -> "https://"; end),
+        ?assertEqual({error, invalid_uri}, token_url())
     after
         meck:unload()
     end.

Reply via email to