Skip to content

Commit

Permalink
fix proxy handling
Browse files Browse the repository at this point in the history
In previous implementation the proxy handling was buggy and incomplete.
This patch add the following:

- with HTTP connections, we connect to the proxy and pass the URL as
  absolute folowing the RFC
- with HTTPS connections we are using CONNECT and the connection is then
  upgraded to SSL.
- add a {connect, ProxyHost, ProxyPort} option to the proxy setting so
  you can force hackney to use connect to a proxy.
- pass the Proxy-Authorization header when the proxy need
  authentication.

fix #76
  • Loading branch information
benoitc committed Mar 2, 2014
1 parent fbc4a44 commit a21e880
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 73 deletions.
117 changes: 93 additions & 24 deletions src/hackney.erl
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,13 @@ request(Method, #hackney_url{}=URL, Headers, Body, Options0) ->
{basic_auth, {User, Password}})
end,

Request = make_request(Method, URL, Headers, Body),

case maybe_proxy(Transport, Host, Port, Options) of
{ok, Ref, AbsolutePath} ->
Request = make_request(Method, URL, Headers, Body,
Options, AbsolutePath),
send_request(Ref, Request);
{ok, Ref} ->
Request = make_request(Method, URL, Headers, Body, Options, false),
send_request(Ref, Request);
Error ->
Error
Expand Down Expand Up @@ -513,10 +516,26 @@ stop_async(Ref) ->

%% internal functions
%%
make_request(Method, #hackney_url{}=URL, Headers, Body) ->
%%
make_request(connect, #hackney_url{}=URL, Headers, Body, _, _) ->
#hackney_url{host = Host,
port = Port}= URL,
Path = iolist_to_binary([Host, ":", integer_to_list(Port)]),
{connect, Path, Headers, Body};
make_request(Method, #hackney_url{}=URL, Headers0, Body, Options, true) ->
Path = hackney_url:unparse_url(URL),
Headers = case proplists:get_value(proxy_auth, Options) of
undefined ->
Headers0;
{User, Pwd} ->
Credentials = base64:encode(<< User/binary, ":", Pwd/binary >>),
Headers0 ++ [{<<"Proxy-Authorization">>,
<<"Basic ", Credentials/binary>>}]
end,
{Method, Path, Headers, Body};
make_request(Method, #hackney_url{}=URL, Headers, Body, _, _) ->
#hackney_url{path = Path,
qs = Query} = URL,

FinalPath = case Query of
<<>> ->
Path;
Expand All @@ -528,27 +547,45 @@ make_request(Method, #hackney_url{}=URL, Headers, Body) ->

maybe_proxy(Transport, Host, Port, Options)
when is_list(Host), is_integer(Port), is_list(Options) ->

case proplists:get_value(proxy, Options) of
Url when is_binary(Url) orelse is_list(Url) ->
ProxyOpts = [{basic_auth, proplists:get_value(proxy_auth,
Options)}],
#hackney_url{transport=PTransport} = hackney_url:parse_url(Url),

if PTransport =/= Transport ->
{error, invalid_proxy_transport};
true ->
hackney_http_proxy:connect(Url, ProxyOpts, Host, Port, Options)
#hackney_url{transport = PTransport,
host = ProxyHost,
port = ProxyPort} = hackney_url:parse_url(Url),
ProxyAuth = proplists:get_value(proxy_auth, Options),
case Transport of
hackney_ssl_transport ->
case PTransport of
hackney_tcp_transport ->
do_connect(ProxyHost, ProxyPort, ProxyAuth,
Transport, Host, Port, Options);
_ ->
{error, invalid_proxy_transport}
end;
_ ->
case hackney_connect:connect(Transport, Host, Port,
Options, true) of
{ok, Ref} -> {ok, Ref, true};
Error -> Error
end
end;
{ProxyHost, ProxyPort} ->
Netloc = iolist_to_binary([ProxyHost, ":",
integer_to_list(ProxyPort)]),
Scheme = hackney_url:transport_scheme(Transport),
Url = #hackney_url{scheme=Scheme, netloc=Netloc},
ProxyOpts = [{basic_auth, proplists:get_value(proxy_auth,
Options)}],
hackney_http_proxy:connect(hackney_url:unparse_url(Url), ProxyOpts, Host,
Port, Options, true);
case Transport of
hackney_ssl_transport ->
ProxyAuth = proplists:get_value(proxy_auth, Options),
do_connect(ProxyHost, ProxyPort, ProxyAuth, Transport,
Host, Port, Options);
_ ->
case hackney_connect:connect(Transport, Host, Port,
Options, true) of
{ok, Ref} -> {ok, Ref, true};
Error -> Error
end
end;
{connect, ProxyHost, ProxyPort} ->
ProxyAuth = proplists:get_value(proxy_auth, Options),
do_connect(ProxyHost, ProxyPort, ProxyAuth, Transport, Host,
Port, Options);
{socks5, ProxyHost, ProxyPort} ->
%% create connection options
ProxyUser = proplists:get_value(socks5_user, Options),
Expand Down Expand Up @@ -576,13 +613,44 @@ maybe_proxy(Transport, Host, Port, Options)
{connect_options, ConnectOpts2}),

%% connect using a socks5 proxy
hackney_connect:connect(hackney_socks5, Host, Port, Options1,
true);
hackney_connect:connect(hackney_socks5, Host, Port,
Options1, true);
_ ->
hackney_connect:connect(Transport, Host, Port, Options, true)
end.


do_connect(ProxyHost, ProxyPort, undefined, Transport, Host, Port, Options) ->
do_connect(ProxyHost, ProxyPort, {undefined, <<>>}, Transport, Host,
Port, Options);
do_connect(ProxyHost, ProxyPort, {ProxyUser, ProxyPass}, Transport, Host,
Port, Options) ->
%% create connection options
ConnectOpts = proplists:get_value(connect_options, Options, []),
ConnectOpts1 = [{connect_host, ProxyHost},
{connect_port, ProxyPort},
{connect_transport, Transport},
{connect_user, ProxyUser},
{connect_pass, ProxyPass}| ConnectOpts],

%% ssl options?
Insecure = proplists:get_value(insecure, Options, false),
ConnectOpts2 = case proplists:get_value(ssl_options, Options) of
undefined ->
[{insecure, Insecure}] ++ ConnectOpts1;
SslOpts ->
[{ssl_opttions, SslOpts},
{insecure, Insecure}] ++ ConnectOpts1
end,

Options1 = lists:keystore(connect_options, 1, Options,
{connect_options, ConnectOpts2}),

%% connect using a socks5 proxy
hackney_connect:connect(hackney_http_connect, Host, Port, Options1, true).



maybe_redirect({ok, _}=Resp, _Req, _Tries) ->
Resp;
maybe_redirect({ok, S, H, #client{follow_redirect=true,
Expand Down Expand Up @@ -650,7 +718,8 @@ redirect(Client0, {Method, NewLocation, Headers, Body}) ->
#hackney_url{transport=RedirectTransport,
host=RedirectHost,
port=RedirectPort}=RedirectUrl,
RedirectRequest = make_request(Method, RedirectUrl, Headers, Body),
RedirectRequest = make_request(Method, RedirectUrl, Headers, Body,
Client#client.options, false),
%% make a request without any redirection
#client{transport=Transport,
host=Host,
Expand Down
207 changes: 207 additions & 0 deletions src/hackney_http_connect.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
%%% -*- 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 <[email protected]>
%%%
%%%
-module(hackney_http_connect).

-include_lib("kernel/src/inet_dns.hrl").

-export([messages/1,
connect/3, connect/4,
recv/2, recv/3,
send/2,
setopts/2,
controlling_process/2,
peername/1,
close/1,
sockname/1]).

-define(TIMEOUT, infinity).

-type socks5_socket() :: {atom(), inet:socket()}.
-export_type([socks5_socket/0]).

%% @doc Atoms used to identify messages in {active, once | true} mode.
messages({hackney_ssl_transport, _}) ->
{ssl, ssl_closed, ssl_error};
messages({_, _}) ->
{tcp, tcp_closed, tcp_error}.


connect(Host, Port, Opts) ->
connect(Host, Port, Opts, infinity).

connect(Host, Port, Opts, Timeout) when is_list(Host), is_integer(Port),
(Timeout =:= infinity orelse is_integer(Timeout)) ->

%% get the proxy host and port from the options
ProxyHost = proplists:get_value(connect_host, Opts),
ProxyPort = proplists:get_value(connect_port, Opts),
Transport = proplists:get_value(connect_transport, Opts),

case gen_tcp:connect(ProxyHost, ProxyPort, [binary, {packet, 0},
{keepalive, true},
{active, false}]) of
{ok, Socket} ->
case do_handshake(Socket, Host, Port, Opts) of
ok ->
case Transport of
hackney_ssl_transport ->
SslOpts0 = proplists:get_value(ssl_options, Opts),
Insecure = proplists:get_value(insecure, Opts),

SslOpts = case {SslOpts0, Insecure} of
{undefined, true} ->
[{verify, verify_none},
{reuse_sessions, true}];
{undefined, _} ->
[];
_ ->
SslOpts0
end,
%% upgrade the tcp connection
case ssl:connect(Socket, SslOpts) of
{ok, SslSocket} ->
{ok, {Transport, SslSocket}};
Error ->
Error
end;
_ ->
{ok, {Transport, Socket}}
end;
Error ->
Error
end;
Error ->
Error
end.

recv(Socket, Length) ->
recv(Socket, Length, infinity).

%% @doc Receive a packet from a socket in passive mode.
%% @see gen_tcp:recv/3
-spec recv(socks5_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(socks5_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(socks5_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(socks5_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(socks5_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(socks5_socket()) -> ok.
close({Transport, Socket}) ->
Transport:close(Socket).

%% @doc Get the local address and port of a socket
%% @see inet:sockname/1
-spec sockname(socks5_socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
sockname({Transport, Socket}) ->
Transport:sockname(Socket).

%% private functions
do_handshake(Socket, Host, Port, Options) ->
ProxyUser = proplists:get_value(connect_user, Options),
ProxyPass = proplists:get_value(connect_pass, Options, <<>>),
ProxyPort = proplists:get_value(connect_port, Options),

%% set defaults headers
HostHdr = case ProxyPort of
true ->
list_to_binary(Host);
false ->
iolist_to_binary([Host, ":", integer_to_list(Port)])
end,
UA = hackney_request: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: ", Credentials/binary >>]
end,
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 gen_tcp:send(Socket, Payload) of
ok ->
check_response(Socket);
Error ->
Error
end.

check_response(Socket) ->
Parser = http_parser:parse([response]),
case gen_tcp:recv(Socket, 0, ?TIMEOUT) of
{ok, Data} ->
check_response(http_parser:execute(Parser, Data), Socket, false);
Error ->
Error
end.

check_response({done, _}, _Socket, Status) ->
Status;
check_response({more, Parser}, Socket, Status) ->
case gen_tcp:recv(Socket, 0, ?TIMEOUT) of
{ok, Data} ->
check_response(http_parser:execute(Parser, Data), Socket, Status);
Error ->
Error
end;
check_response({response, _, Status, _, Parser}, Socket, _)
when Status =:= 200 orelse Status =:= 201 ->
check_response(http_parser:execute(Parser), Socket, ok);
check_response({response, _, Status, _, Parser}, Socket, _) ->
check_response(http_parser:execute(Parser), Socket, {error, Status});
check_response({{header, _}, Parser}, Socket, Status) ->
check_response(http_parser:execute(Parser), Socket, Status);
check_response({headers_complete, Parser}, Socket, Status) ->
check_response(http_parser:execute(Parser), Socket, Status);
check_response({ok, _, Parser}, Socket, Status) ->
check_response(http_parser:execute(Parser), Socket, Status);
check_response({more, Parser, _}, Socket, Status) ->
case gen_tcp:recv(Socket, 0, ?TIMEOUT) of
{ok, Data} ->
check_response(http_parser:execute(Parser, Data), Socket, Status);
Error ->
Error
end;
check_response(Error, _, _) ->
Error.
Loading

0 comments on commit a21e880

Please sign in to comment.