%%% -*- 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 %%% %%% -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 Pid to Socket. %% @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(<>), 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 = <>, 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.