Packages
hackney
1.9.0
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/hackney_connect.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
-module(hackney_connect).
-export([connect/3, connect/4, connect/5,
create_connection/4, create_connection/5,
maybe_connect/1,
reconnect/4,
set_sockopts/2,
ssl_opts/2,
check_or_close/1,
close/1,
is_pool/1]).
-export([partial_chain/1]).
-include("hackney.hrl").
-include_lib("hackney_internal.hrl").
-include_lib("public_key/include/OTP-PUB-KEY.hrl").
connect(Transport, Host, Port) ->
connect(Transport, Host, Port, []).
connect(Transport, Host, Port, Options) ->
connect(Transport, Host, Port, Options, false).
connect(Transport, Host, Port, Options, Dynamic) when is_binary(Host) ->
connect(Transport, binary_to_list(Host), Port, Options, Dynamic);
connect(Transport, Host, Port, Options, Dynamic) ->
?report_debug("connect", [{transport, Transport},
{host, Host},
{port, Port},
{dynamic, Dynamic}]),
case create_connection(Transport, idna:utf8_to_ascii(Host), Port,
Options, Dynamic) of
{ok, #client{request_ref=Ref}} ->
{ok, Ref};
Error ->
Error
end.
%% @doc create a connection and return a client state
create_connection(Transport, Host, Port, Options) ->
create_connection(Transport, Host, Port, Options, true).
create_connection(Transport, Host, Port, Options, Dynamic)
when is_list(Options) ->
Netloc = case {Transport, Port} of
{hackney_tcp, 80} -> list_to_binary(Host);
{hackney_ssl, 443} -> list_to_binary(Host);
_ ->
iolist_to_binary([Host, ":", integer_to_list(Port)])
end,
%% default timeout
Timeout = proplists:get_value(recv_timeout, Options, ?RECV_TIMEOUT),
FollowRedirect = proplists:get_value(follow_redirect, Options, false),
MaxRedirect = proplists:get_value(max_redirect, Options, 5),
ForceRedirect = proplists:get_value(force_redirect, Options, false),
Async = proplists:get_value(async, Options, false),
StreamTo = proplists:get_value(stream_to, Options, false),
WithBody = proplists:get_value(with_body, Options, false),
MaxBody = proplists:get_value(max_body, Options),
%% get mod metrics
Engine = metrics:init(hackney_util:mod_metrics()),
%% initial state
InitialState = #client{mod_metrics=Engine,
transport=Transport,
host=Host,
port=Port,
netloc=Netloc,
options=Options,
dynamic=Dynamic,
recv_timeout=Timeout,
follow_redirect=FollowRedirect,
max_redirect=MaxRedirect,
retries=MaxRedirect,
force_redirect=ForceRedirect,
async=Async,
with_body=WithBody,
max_body=MaxBody,
stream_to=StreamTo,
buffer = <<>>},
%% if we use a pool then checkout the connection from the pool, else
%% connect the socket to the remote
%%
reconnect(Host, Port, Transport, InitialState).
%% @doc connect a socket and create a client state.
%%
maybe_connect(#client{state=closed, redirect=nil}=Client) ->
%% the socket has been closed, reconnect it.
#client{transport=Transport,
host=Host,
port=Port} = Client,
reconnect(Host, Port, Transport, Client);
maybe_connect(#client{state=closed, redirect=Redirect}=Client) ->
%% connection closed after a redirection, reinit the options and
%% reconnect it.
{Transport, Host, Port, Options} = Redirect,
Client1 = Client#client{options=Options,
redirect=nil},
reconnect(Host, Port, Transport, Client1);
maybe_connect(#client{redirect=nil}=Client) ->
{ok, check_mod_metrics(Client)};
maybe_connect(#client{redirect=Redirect}=Client) ->
%% reinit the options and reconnect the client
{Transport, Host, Port, Options} = Redirect,
reconnect(Host, Port, Transport, Client#client{options=Options,
redirect=nil}).
check_or_close(#client{socket=nil}=Client) ->
Client;
check_or_close(Client) ->
case is_pool(Client) of
false ->
close(Client);
true ->
#client{socket=Socket, socket_ref=Ref, pool_handler=Handler}=Client,
_ = Handler:checkin(Ref, Socket),
Client#client{socket=nil, state=closed}
end.
%% @doc add set sockets options in the client
set_sockopts(#client{transport=Transport, socket=Skt}, Options) ->
Transport:setopts(Skt, Options).
%% @doc close the client
%%
%%
close(#client{socket=nil}=Client) ->
Client#client{state = closed};
close(#client{transport=Transport, socket=Skt}=Client) ->
Transport:close(Skt),
Client#client{state = closed, socket=nil};
close(Ref) when is_reference(Ref) ->
hackney_manager:close_request(Ref).
%% @doc get current pool pid or name used by a client if needed
is_pool(#client{options=Opts}) ->
UseDefaultPool = use_default_pool(),
case proplists:get_value(pool, Opts) of
false ->
false;
undefined when UseDefaultPool =:= true ->
true;
undefined ->
false;
_ ->
true
end.
reconnect(Host, Port, Transport, State) ->
%% if we use a pool then checkout the connection from the pool, else
%% connect the socket to the remote
case is_pool(State) of
false ->
%% the client won't use any pool
do_connect(Host, Port, Transport, check_mod_metrics(State));
true ->
socket_from_pool(Host, Port, Transport, check_mod_metrics(State))
end.
%%
%% internal functions
%%
socket_from_pool(Host, Port, Transport, Client0) ->
PoolHandler = hackney_app:get_app_env(pool_handler, hackney_pool),
PoolName = proplists:get_value(pool, Client0#client.options, default),
Metrics = Client0#client.mod_metrics,
%% new request
{_RequestRef, Client} = hackney_manager:new_request(Client0),
case PoolHandler:checkout(Host, Port, Transport, Client) of
{ok, Ref, Skt} ->
?report_debug("reuse a connection", [{pool, PoolName}]),
_ = metrics:update_meter(Metrics, [hackney_pool, PoolName, take_rate], 1),
_ = metrics:increment_counter(Metrics, [hackney_pool, Host, reuse_connection]),
Client1 = Client#client{socket=Skt,
socket_ref=Ref,
pool_handler=PoolHandler,
state = connected},
hackney_manager:update_state(Client1),
{ok, Client1};
{error, no_socket, Ref} ->
?report_trace("no socket in the pool", [{pool, PoolName}]),
_ = metrics:increment_counter(Metrics, [hackney_pool, PoolName, no_socket]),
do_connect(Host, Port, Transport, Client#client{socket_ref=Ref},
pool);
Error ->
Error
end.
do_connect(Host, Port, Transport, Client) ->
do_connect(Host, Port, Transport, Client, direct).
do_connect(Host, Port, Transport, #client{mod_metrics=Metrics,
options=Opts}=Client0, Type) ->
Begin = os:timestamp(),
{_RequestRef, Client} = case Type of
pool ->
{Client0#client.request_ref, Client0};
direct ->
hackney_manager:new_request(Client0)
end,
ConnectOpts0 = proplists:get_value(connect_options, Opts, []),
ConnectTimeout = proplists:get_value(connect_timeout, Opts, 8000),
%% handle ipv6
ConnectOpts1 = case lists:member(inet, ConnectOpts0) orelse
lists:member(inet6, ConnectOpts0) of
true ->
ConnectOpts0;
false ->
case hackney_util:is_ipv6(Host) of
true ->
[inet6 | ConnectOpts0];
false ->
ConnectOpts0
end
end,
ConnectOpts = case Transport of
hackney_ssl ->
ConnectOpts1 ++ ssl_opts(Host, Opts);
_ ->
ConnectOpts1
end,
case Transport:connect(Host, Port, ConnectOpts, ConnectTimeout) of
{ok, Skt} ->
?report_trace("new connection", []),
ConnectTime = timer:now_diff(os:timestamp(), Begin)/1000,
_ = metrics:update_histogram(Metrics, [hackney, Host, connect_time], ConnectTime),
_ = metrics:increment_counter(Metrics, [hackney_pool, Host, new_connection]),
Client1 = Client#client{socket=Skt,
state = connected},
hackney_manager:update_state(Client1),
{ok, Client1};
{error, timeout} ->
?report_trace("connect timeout", []),
_ = metrics:increment_counter(Metrics, [hackney, Host, connect_timeout]),
hackney_manager:cancel_request(Client),
{error, connect_timeout};
Error ->
?report_trace("connect error", []),
_ = metrics:increment_counter(Metrics, [hackney, Host, connect_error]),
hackney_manager:cancel_request(Client),
Error
end.
use_default_pool() ->
case application:get_env(hackney, use_default_pool) of
{ok, Val} ->
Val;
_ ->
true
end.
check_mod_metrics(#client{mod_metrics=Mod}=State)
when Mod /= nil, Mod /= undefined ->
State;
check_mod_metrics(State) ->
State#client{mod_metrics=metrics:init(hackney_util:mod_metrics())}.
ssl_opts(Host, Options) ->
case proplists:get_value(ssl_options, Options) of
undefined ->
Insecure = proplists:get_value(insecure, Options, false),
CACerts = certifi:cacerts(),
case Insecure of
true ->
[{verify, verify_none}];
false ->
VerifyFun = {
fun ssl_verify_hostname:verify_fun/3,
[{check_hostname, Host}]
},
[{verify, verify_peer},
{depth, 99},
{cacerts, CACerts},
{partial_chain, fun partial_chain/1},
{verify_fun, VerifyFun}]
end;
SSLOpts ->
SSLOpts
end.
%% code from rebar3 undert BSD license
partial_chain(Certs) ->
Certs1 = lists:reverse([{Cert, public_key:pkix_decode_cert(Cert, otp)} ||
Cert <- Certs]),
CACerts = certifi:cacerts(),
CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts],
case find(fun({_, Cert}) ->
check_cert(CACerts1, Cert)
end, Certs1) of
{ok, Trusted} ->
{trusted_ca, element(1, Trusted)};
_ ->
unknown_ca
end.
extract_public_key_info(Cert) ->
((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo).
check_cert(CACerts, Cert) ->
lists:any(fun(CACert) ->
extract_public_key_info(CACert) == extract_public_key_info(Cert)
end, CACerts).
-spec find(fun(), list()) -> {ok, term()} | error.
find(Fun, [Head|Tail]) when is_function(Fun) ->
case Fun(Head) of
true ->
{ok, Head};
false ->
find(Fun, Tail)
end;
find(_Fun, []) ->
error.