Packages
hackney
2.0.1
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_http_connect.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2012-2014 Benoît Chesneau <benoitc@e-engura.org>
%%%
%%%
-module(hackney_http_connect).
-export([messages/1,
connect/3, connect/4,
recv/2, recv/3,
send/2,
setopts/2,
controlling_process/2,
peername/1,
close/1,
shutdown/2,
sockname/1]).
-define(DEFAULT_RECV_TIMEOUT, infinity).
-type http_socket() :: {atom(), inet:socket()}.
-export_type([http_socket/0]).
%% Use hackney_ssl for SSL options (was hackney_connection)
ssl_opts(Host, Opts) ->
hackney_ssl:ssl_opts(Host, Opts).
%% @doc Atoms used to identify messages in {active, once | true} mode.
messages({hackney_ssl, _}) ->
{ssl, ssl_closed, ssl_error};
messages({_, _}) ->
{tcp, tcp_closed, tcp_error}.
connect(ProxyHost, ProxyPort, Opts) ->
connect(ProxyHost, ProxyPort, Opts, infinity).
connect(ProxyHost, ProxyPort, Opts, Timeout)
when is_list(ProxyHost), is_integer(ProxyPort),
(Timeout =:= infinity orelse is_integer(Timeout)) ->
%% get the host and port to connect from the options
Host = proplists:get_value(connect_host, Opts),
Port = proplists:get_value(connect_port, Opts),
Transport = proplists:get_value(connect_transport, Opts),
ProxyTransport = proplists:get_value(proxy_transport, Opts, tcp),
%% filter connection options
AcceptedOpts = [linger, nodelay, send_timeout,
send_timeout_close, raw, inet6],
BaseOpts = [binary, {active, false}, {packet, 0}, {keepalive, true},
{nodelay, true}],
ConnectOpts = hackney_util:filter_options(Opts, AcceptedOpts, BaseOpts),
%% connect to the proxy (TCP or TLS based on proxy_transport)
case connect_to_proxy(ProxyHost, ProxyPort, ProxyTransport, ConnectOpts, Opts) of
{ok, ProxySocket} ->
case do_handshake(ProxySocket, ProxyTransport, Host, Port, Opts) of
ok ->
%% if we are connecting to a remote https source, we
%% upgrade the tunnel to handle SSL (end-to-end encryption).
case Transport of
hackney_ssl ->
SSLOpts = ssl_opts(Host, Opts),
%% upgrade the tunnel to TLS
case ssl:connect(ProxySocket, SSLOpts) of
{ok, SslSocket} ->
{ok, {Transport, SslSocket}};
Error ->
_ = close_proxy_socket(ProxySocket, ProxyTransport),
Error
end;
_ ->
%% For HTTP targets, wrap the socket appropriately
case ProxyTransport of
ssl ->
%% Proxy connection is TLS, but target is HTTP
{ok, {hackney_ssl, ProxySocket}};
_ ->
{ok, {Transport, ProxySocket}}
end
end;
Error ->
_ = close_proxy_socket(ProxySocket, ProxyTransport),
Error
end;
Error ->
Error
end.
%% Connect to proxy using TCP or TLS
connect_to_proxy(ProxyHost, ProxyPort, ssl, ConnectOpts, Opts) ->
%% HTTPS proxy: connect with TLS
case hackney_happy:connect(ProxyHost, ProxyPort, ConnectOpts) of
{ok, TcpSocket} ->
ProxySslOpts = proplists:get_value(proxy_ssl_options, Opts, []),
%% Add SNI for the proxy
SslOpts = [{server_name_indication, ProxyHost} | ProxySslOpts],
case ssl:connect(TcpSocket, SslOpts) of
{ok, SslSocket} ->
{ok, SslSocket};
Error ->
gen_tcp:close(TcpSocket),
Error
end;
Error ->
Error
end;
connect_to_proxy(ProxyHost, ProxyPort, tcp, ConnectOpts, _Opts) ->
%% HTTP proxy: plain TCP connection
hackney_happy:connect(ProxyHost, ProxyPort, ConnectOpts).
%% Close proxy socket based on transport type
close_proxy_socket(Socket, ssl) ->
ssl:close(Socket);
close_proxy_socket(Socket, tcp) ->
gen_tcp:close(Socket).
recv(Socket, Length) ->
recv(Socket, Length, infinity).
%% @doc Receive a packet from a socket in passive mode.
%% @see gen_tcp:recv/3
-spec recv(http_socket(), non_neg_integer(), timeout())
-> {ok, any()} | {error, closed | atom()}.
recv({Transport, Socket}, Length, Timeout) ->
Transport:recv(Socket, Length, Timeout).
%% @doc Send a packet on a socket.
%% @see gen_tcp:send/2
-spec send(http_socket(), iolist()) -> ok | {error, atom()}.
send({Transport, Socket}, Packet) ->
Transport:send(Socket, Packet).
%% @doc Set one or more options for a socket.
%% @see inet:setopts/2
-spec setopts(http_socket(), list()) -> ok | {error, atom()}.
setopts({Transport, Socket}, Opts) ->
Transport:setopts(Socket, Opts).
%% @doc Assign a new controlling process <em>Pid</em> to <em>Socket</em>.
%% @see gen_tcp:controlling_process/2
-spec controlling_process(http_socket(), pid())
-> ok | {error, closed | not_owner | atom()}.
controlling_process({Transport, Socket}, Pid) ->
Transport:controlling_process(Socket, Pid).
%% @doc Return the address and port for the other end of a connection.
%% @see inet:peername/1
-spec peername(http_socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
peername({Transport, Socket}) ->
Transport:peername(Socket).
%% @doc Close a socks5 socket.
%% @see gen_tcp:close/1
-spec close(http_socket()) -> ok.
close({Transport, Socket}) ->
Transport:close(Socket).
%% @doc Immediately close a socket in one or two directions.
%% @see gen_tcp:shutdown/2
-spec shutdown(http_socket(), read | write | read_write) -> ok.
shutdown({Transport, Socket}, How) ->
Transport:shutdown(Socket, How).
%% @doc Get the local address and port of a socket
%% @see inet:sockname/1
-spec sockname(http_socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
sockname({Transport, Socket}) ->
Transport:sockname(Socket).
%% private functions
do_handshake(Socket, ProxyTransport, Host, Port, Options) ->
ProxyUser = proplists:get_value(connect_user, Options),
ProxyPass = proplists:get_value(connect_pass, Options, <<>>),
ProxyPort = proplists:get_value(connect_port, Options),
RecvTimeout = proplists:get_value(recv_timeout, Options, ?DEFAULT_RECV_TIMEOUT),
OnConnectResponse = proplists:get_value(on_connect_response, Options),
ProxyAuthFun = proplists:get_value(proxy_auth_fun, Options),
%% set defaults headers
HostHdr = case ProxyPort of
80 ->
list_to_binary(Host);
_ ->
iolist_to_binary([Host, ":", integer_to_list(Port)])
end,
UA = hackney:default_ua(),
Headers0 = [<<"Host: ", HostHdr/binary>>,
<<"User-Agent: ", UA/binary >>],
Headers = case ProxyUser of
undefined ->
Headers0;
_ ->
Credentials = base64:encode(<<ProxyUser/binary, ":",
ProxyPass/binary>>),
Headers0 ++ [<< "Proxy-Authorization: Basic ", Credentials/binary >>]
end,
do_handshake_with_auth(Socket, ProxyTransport, Host, Port, Headers,
RecvTimeout, OnConnectResponse, ProxyAuthFun, 0).
%% Handshake with optional proxy authentication callback (issue #115)
%% MaxRetries limits challenge-response rounds to prevent infinite loops
-define(MAX_AUTH_RETRIES, 5).
do_handshake_with_auth(_Socket, _ProxyTransport, _Host, _Port, _Headers,
_RecvTimeout, _OnConnectResponse, _ProxyAuthFun, Retries)
when Retries >= ?MAX_AUTH_RETRIES ->
{error, proxy_auth_failed};
do_handshake_with_auth(Socket, ProxyTransport, Host, Port, Headers,
RecvTimeout, OnConnectResponse, ProxyAuthFun, Retries) ->
Path = iolist_to_binary([Host, ":", integer_to_list(Port)]),
Payload = [<< "CONNECT ", Path/binary, " HTTP/1.1", "\r\n" >>,
hackney_bstr:join(lists:reverse(Headers), <<"\r\n">>),
<<"\r\n\r\n">>],
case proxy_send(Socket, ProxyTransport, Payload) of
ok ->
case check_response(Socket, ProxyTransport, RecvTimeout) of
{ok, Status, RespHeaders} ->
%% Success - call the callback if provided (issue #438)
maybe_call_on_connect_response(OnConnectResponse, Status, RespHeaders),
ok;
{proxy_auth_required, StatusCode, RespHeaders} when ProxyAuthFun =/= undefined ->
%% 407 response - try custom auth callback
handle_proxy_auth(Socket, ProxyTransport, Host, Port, Headers,
RecvTimeout, OnConnectResponse, ProxyAuthFun,
StatusCode, RespHeaders, Retries);
{proxy_auth_required, _StatusCode, _RespHeaders} ->
%% 407 but no auth callback provided
{error, proxy_auth_required};
Error ->
Error
end;
Error ->
Error
end.
%% Handle 407 Proxy Authentication Required
handle_proxy_auth(Socket, ProxyTransport, Host, Port, Headers,
RecvTimeout, OnConnectResponse, ProxyAuthFun,
StatusCode, RespHeaders, Retries) ->
%% Call the user's auth function
case ProxyAuthFun(StatusCode, RespHeaders) of
{ok, AuthHeader} when is_binary(AuthHeader) ->
%% Remove any existing Proxy-Authorization and add new one
Headers1 = lists:filter(fun(H) -> not is_proxy_auth_header(H) end, Headers),
Headers2 = Headers1 ++ [<<"Proxy-Authorization: ", AuthHeader/binary>>],
%% Retry with new auth header
do_handshake_with_auth(Socket, ProxyTransport, Host, Port, Headers2,
RecvTimeout, OnConnectResponse, ProxyAuthFun, Retries + 1);
{error, Reason} ->
{error, {proxy_auth_error, Reason}};
_ ->
{error, proxy_auth_failed}
end.
%% Check if a header is a Proxy-Authorization header
is_proxy_auth_header(Header) when is_binary(Header) ->
case binary:match(hackney_bstr:to_lower(Header), <<"proxy-authorization">>) of
{0, _} -> true;
_ -> false
end;
is_proxy_auth_header(_) ->
false.
%% Call the on_connect_response callback if provided
maybe_call_on_connect_response(undefined, _Status, _Headers) ->
ok;
maybe_call_on_connect_response(Fun, Status, Headers) when is_function(Fun, 2) ->
Fun(Status, Headers);
maybe_call_on_connect_response(_Invalid, _Status, _Headers) ->
ok.
%% Send data over proxy socket (TCP or TLS)
proxy_send(Socket, ssl, Data) ->
ssl:send(Socket, Data);
proxy_send(Socket, tcp, Data) ->
gen_tcp:send(Socket, Data).
%% Receive data from proxy socket (TCP or TLS)
proxy_recv(Socket, ssl, Length, Timeout) ->
ssl:recv(Socket, Length, Timeout);
proxy_recv(Socket, tcp, Length, Timeout) ->
gen_tcp:recv(Socket, Length, Timeout).
%% Read the full HTTP response (until \r\n\r\n) before returning.
%% This fixes issue #536 where partial reads cause SSL handshake failures.
%% RecvTimeout is used to respect recv_timeout during proxy handshake (issue #569).
check_response(Socket, ProxyTransport, RecvTimeout) ->
check_response(Socket, ProxyTransport, RecvTimeout, <<>>).
check_response(Socket, ProxyTransport, RecvTimeout, Buffer) ->
case proxy_recv(Socket, ProxyTransport, 0, RecvTimeout) of
{ok, Data} ->
NewBuffer = <<Buffer/binary, Data/binary>>,
case binary:match(NewBuffer, <<"\r\n\r\n">>) of
{_Pos, 4} ->
%% Found end of headers, now parse the response
parse_response(NewBuffer);
nomatch ->
%% Keep reading until we get the full response headers
check_response(Socket, ProxyTransport, RecvTimeout, NewBuffer)
end;
{error, timeout} ->
%% Return timeout error instead of generic closed error
{error, timeout};
Error ->
Error
end.
%% Parse the HTTP response and extract status and headers (issue #438)
parse_response(Response) ->
case binary:split(Response, <<"\r\n">>) of
[StatusLine, Rest] ->
Headers = parse_headers(Rest),
case check_status_code(StatusLine) of
ok ->
{ok, StatusLine, Headers};
{auth_required, Code} ->
%% 407 Proxy Authentication Required - return specially for auth callback
{proxy_auth_required, Code, Headers};
Error ->
Error
end;
_ ->
{error, proxy_error}
end.
%% Check if status code indicates success or auth required
check_status_code(<< "HTTP/1.1 200", _/bits >>) -> ok;
check_status_code(<< "HTTP/1.1 201", _/bits >>) -> ok;
check_status_code(<< "HTTP/1.0 200", _/bits >>) -> ok;
check_status_code(<< "HTTP/1.0 201", _/bits >>) -> ok;
check_status_code(<< "HTTP/1.1 407", _/bits >>) -> {auth_required, 407};
check_status_code(<< "HTTP/1.0 407", _/bits >>) -> {auth_required, 407};
check_status_code(StatusLine) ->
error_logger:error_msg("proxy error: ~w~n", [StatusLine]),
{error, proxy_error}.
%% Parse headers into a list of {Name, Value} tuples
parse_headers(Data) ->
%% Remove trailing \r\n\r\n
HeaderData = case binary:split(Data, <<"\r\n\r\n">>) of
[H, _] -> H;
[H] -> H
end,
Lines = binary:split(HeaderData, <<"\r\n">>, [global]),
parse_header_lines(Lines, []).
parse_header_lines([], Acc) ->
lists:reverse(Acc);
parse_header_lines([<<>> | Rest], Acc) ->
parse_header_lines(Rest, Acc);
parse_header_lines([Line | Rest], Acc) ->
case binary:split(Line, <<": ">>) of
[Name, Value] ->
parse_header_lines(Rest, [{Name, Value} | Acc]);
[Name] ->
%% Header with no value or malformed, try splitting on just ":"
case binary:split(Name, <<":">>) of
[N, V] ->
parse_header_lines(Rest, [{N, hackney_bstr:trim(V)} | Acc]);
_ ->
parse_header_lines(Rest, Acc)
end
end.