Skip to content

Commit 6c3a73a

Browse files
authored
Merge branch 'master' into master
2 parents 9308343 + 1dd4b92 commit 6c3a73a

13 files changed

+918
-60
lines changed

NOTICE

+3
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ Copyright (c) 2009, Erlang Training and Consulting Ltd.
2121
Copyright (C) 1998 - 2014, Daniel Stenberg, <[email protected]>, et al.
2222

2323
*) hackney_trace (C) 2015 under the Erlang Public License
24+
25+
*) hackney_cidr is based on inet_cidr 1.2.1. vendored for customer purpose.
26+
Copyright (c) 2024, Enki Multimedia , MIT License

include/hackney.hrl

+1
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@
5959

6060
-define(HTTP_PROXY_ENV_VARS, ["http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY"]).
6161
-define(HTTPS_PROXY_ENV_VARS, ["https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"]).
62+
-define(HTTP_NO_PROXY_ENV_VARS, ["no_proxy", "NO_PROXY"]).

src/hackney.erl

+130-17
Original file line numberDiff line numberDiff line change
@@ -311,17 +311,18 @@ request(Method, #hackney_url{}=URL0, Headers0, Body, Options0) ->
311311
URL = hackney_url:normalize(URL0, PathEncodeFun),
312312

313313
?report_trace("request", [{method, Method},
314-
{url, URL},
315-
{headers, Headers0},
316-
{body, Body},
317-
{options, Options0}]),
314+
{url, URL},
315+
{headers, Headers0},
316+
{body, Body},
317+
{options, Options0}]),
318318

319319
#hackney_url{transport=Transport,
320-
host = Host,
321-
port = Port,
322-
user = User,
323-
password = Password,
324-
scheme = Scheme} = URL,
320+
host = Host,
321+
port = Port,
322+
user = User,
323+
password = Password,
324+
scheme = Scheme} = URL,
325+
325326

326327
Options = case User of
327328
<<>> ->
@@ -676,14 +677,22 @@ maybe_proxy(Transport, Scheme, Host, Port, Options)
676677
end.
677678

678679
maybe_proxy_from_env(Transport, _Scheme, Host, Port, Options, true) ->
679-
?report_debug("request without proxy", []),
680+
?report_debug("no proxy env is forced, request without proxy", []),
680681
hackney_connect:connect(Transport, Host, Port, Options, true);
681682
maybe_proxy_from_env(Transport, Scheme, Host, Port, Options, _) ->
682683
case get_proxy_env(Scheme) of
683684
{ok, Url} ->
684-
proxy_from_url(Url, Transport, Host, Port, Options);
685+
NoProxyEnv = get_no_proxy_env(),
686+
case match_no_proxy_env(NoProxyEnv, Host) of
687+
false ->
688+
?report_debug("request with proxy", [{proxy, Url}, {host, Host}]),
689+
proxy_from_url(Url, Transport, Host, Port, Options);
690+
true ->
691+
?report_debug("request without proxy", []),
692+
hackney_connect:connect(Transport, Host, Port, Options, true)
693+
end;
685694
false ->
686-
?report_debug("request without proxy", []),
695+
?report_debug("no proxy env setup, request without proxy", []),
687696
hackney_connect:connect(Transport, Host, Port, Options, true)
688697
end.
689698

@@ -705,17 +714,121 @@ proxy_from_url(Url, Transport, Host, Port, Options) ->
705714
end
706715
end.
707716

717+
get_no_proxy_env() ->
718+
case application:get_env(hackney, no_proxy) of
719+
undefined ->
720+
case get_no_proxy_env(?HTTP_NO_PROXY_ENV_VARS) of
721+
false ->
722+
application:set_env(hackney, no_proxy, false),
723+
false;
724+
NoProxyEnv ->
725+
parse_no_proxy_env(NoProxyEnv, [])
726+
end;
727+
{ok, NoProxyEnv} ->
728+
NoProxyEnv
729+
end.
730+
731+
get_no_proxy_env([Key | Rest]) ->
732+
case os:getenv(Key) of
733+
false -> get_no_proxy_env(Rest);
734+
NoProxyStr ->
735+
lists:usort(string:tokens(NoProxyStr, ","))
736+
end;
737+
get_no_proxy_env([]) ->
738+
false.
739+
740+
parse_no_proxy_env(["*" | _], _Acc) ->
741+
application:set_env(hackney, no_proxy, '*'),
742+
'*';
743+
parse_no_proxy_env([S | Rest], Acc) ->
744+
try
745+
CIDR = hackney_cidr:parse(S),
746+
parse_no_proxy_env(Rest, [{cidr, CIDR} | Acc])
747+
catch
748+
_:_ ->
749+
Labels = string:tokens(S, "."),
750+
parse_no_proxy_env(Rest, [{host, lists:reverse(Labels)}])
751+
end;
752+
parse_no_proxy_env([], Acc) ->
753+
NoProxy = lists:reverse(Acc),
754+
application:set_env(hackney, no_proxy, NoProxy),
755+
NoProxy.
756+
757+
match_no_proxy_env(false, _Host) -> false;
758+
match_no_proxy_env('*', _Host) -> true;
759+
match_no_proxy_env(Patterns, Host) ->
760+
do_match_no_proxy_env(Patterns, undefined, undefined, Host).
761+
762+
do_match_no_proxy_env([{cidr, _CIDR} | _]=Patterns, undefined, Labels, Host) ->
763+
Addrs = case inet:parse_address(Host) of
764+
{ok, Addr} -> [Addr];
765+
_ -> getaddrs(Host)
766+
end,
767+
do_match_no_proxy_env(Patterns, Addrs, Labels, Host);
768+
do_match_no_proxy_env([{cidr, CIDR} | Rest], Addrs, Labels, Host) ->
769+
case test_host_cidr(Addrs, CIDR) of
770+
true -> true;
771+
false -> do_match_no_proxy_env(Rest, Addrs, Labels, Host)
772+
end;
773+
do_match_no_proxy_env([{host, _Labels} | _] = Patterns, Addrs, undefined, Host) ->
774+
HostLabels = string:tokens(Host, "."),
775+
do_match_no_proxy_env(Patterns, Addrs, lists:reverse(HostLabels), Host);
776+
do_match_no_proxy_env([{host, Labels} | Rest], Addrs, HostLabels, Host) ->
777+
case test_host_labels(Labels, HostLabels) of
778+
true -> true;
779+
false -> do_match_no_proxy_env(Rest, Addrs, Labels, Host)
780+
end;
781+
do_match_no_proxy_env([], _, _, _) ->
782+
false.
783+
784+
test_host_labels(["*" | R1], [_ | R2]) -> test_host_labels(R1, R2);
785+
test_host_labels([ AR1], [AR2]) -> test_host_labels(R1, R2);
786+
test_host_labels([], _) -> true;
787+
test_host_labels(_, _) -> false.
788+
789+
test_host_cidr([Addr, Rest], CIDR) ->
790+
case hackney_cidr:contains(CIDR, Addr) of
791+
true -> true;
792+
false -> test_host_cidr(Rest, CIDR)
793+
end;
794+
test_host_cidr([], _) ->
795+
false.
796+
797+
getaddrs(Host) ->
798+
IP4Addrs = case inet:getaddrs(Host, inet) of
799+
{ok, Addrs} -> Addrs;
800+
{error, nxdomain} -> []
801+
end,
802+
case inet:getaddrs(Host, inet6) of
803+
{ok, IP6Addrs} -> [IP6AddrsIP4Addrs];
804+
{error, nxdomain} -> IP4Addrs
805+
end.
806+
708807
get_proxy_env(https) ->
709-
get_proxy_env(?HTTPS_PROXY_ENV_VARS);
808+
case application:get_env(hackney, https_proxy) of
809+
undefined ->
810+
ProxyEnv = do_get_proxy_env(?HTTPS_PROXY_ENV_VARS),
811+
application:set_env(hackney, https_proxy, ProxyEnv),
812+
ProxyEnv;
813+
{ok, Cached} ->
814+
Cached
815+
end;
710816
get_proxy_env(S) when S =:= http; S =:= http_unix ->
711-
get_proxy_env(?HTTP_PROXY_ENV_VARS);
817+
case application:get_env(hackney, http_proxy) of
818+
undefined ->
819+
ProxyEnv = do_get_proxy_env(?HTTP_PROXY_ENV_VARS),
820+
application:set_env(hackney, http_proxy, ProxyEnv),
821+
ProxyEnv;
822+
{ok, Cached} ->
823+
Cached
824+
end.
712825

713-
get_proxy_env([Var | Rest]) ->
826+
do_get_proxy_env([Var | Rest]) ->
714827
case os:getenv(Var) of
715-
false -> get_proxy_env(Rest);
828+
false -> do_get_proxy_env(Rest);
716829
Url -> {ok, Url}
717830
end;
718-
get_proxy_env([]) ->
831+
do_get_proxy_env([]) ->
719832
false.
720833

721834
do_connect(ProxyHost, ProxyPort, undefined, Transport, Host, Port, Options) ->

src/hackney_connection.erl

+3-17
Original file line numberDiff line numberDiff line change
@@ -102,27 +102,13 @@ connect_options(hackney_local_tcp, _Host, ClientOptions) ->
102102
proplists:get_value(connect_options, ClientOptions, []);
103103

104104
connect_options(Transport, Host, ClientOptions) ->
105-
ConnectOpts0 = proplists:get_value(connect_options, ClientOptions, []),
106-
107-
%% handle ipv6
108-
ConnectOpts1 = case lists:member(inet, ConnectOpts0) orelse
109-
lists:member(inet6, ConnectOpts0) of
110-
true ->
111-
ConnectOpts0;
112-
false ->
113-
case hackney_util:is_ipv6(Host) of
114-
true ->
115-
[inet6 | ConnectOpts0];
116-
false ->
117-
ConnectOpts0
118-
end
119-
end,
105+
ConnectOpts = proplists:get_value(connect_options, ClientOptions, []),
120106

121107
case Transport of
122108
hackney_ssl ->
123-
ConnectOpts1 ++ ssl_opts(Host, ClientOptions);
109+
[{ssl_options, ssl_opts(Host, ClientOptions)} | ConnectOpts];
124110
_ ->
125-
ConnectOpts1
111+
ConnectOpts
126112
end.
127113

128114

src/hackney_happy.erl

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
-module(hackney_happy).
2+
3+
-export([connect/3, connect/4]).
4+
5+
-include("hackney_internal.hrl").
6+
-include_lib("kernel/include/inet.hrl").
7+
8+
-define(TIMEOUT, 250).
9+
-define(CONNECT_TIMEOUT, 5000).
10+
11+
connect(Hostname, Port, Opts) ->
12+
connect(Hostname, Port, Opts, ?CONNECT_TIMEOUT).
13+
14+
connect(Hostname, Port, Opts, Timeout) ->
15+
do_connect(parse_address(Hostname), Port, Opts, Timeout).
16+
17+
do_connect(Hostname, Port, Opts, Timeout) when is_tuple(Hostname) ->
18+
case hackney_cidr:is_ipv6(Hostname) of
19+
true ->
20+
?report_debug("connect using IPv6", [{hostname, Hostname}, {port, Port}]),
21+
gen_tcp:connect(Hostname, Port, [inet6 | Opts], Timeout);
22+
false ->
23+
case hackney_cidr:is_ipv4(Hostname) of
24+
true ->
25+
?report_debug("connect using IPv4", [{hostname, Hostname}, {port, Port}]),
26+
gen_tcp:connect(Hostname, Port, [inet | Opts], Timeout);
27+
false ->
28+
{error, nxdomain}
29+
end
30+
end;
31+
do_connect(Hostname, Port, Opts, Timeout) ->
32+
?report_debug("happy eyeballs, try to connect using IPv6", [{hostname, Hostname}, {port, Port}]),
33+
Self = self(),
34+
{Ipv6Addrs, IPv4Addrs} = getaddrs(Hostname),
35+
{Pid6, MRef6} = spawn_monitor(fun() -> try_connect(Ipv6Addrs, Port, Opts, Self) end),
36+
TRef = erlang:start_timer(?TIMEOUT, self(), try_ipv4),
37+
receive
38+
{'DOWN', MRef6, _Type, _Pid, {happy_connect, OK}} ->
39+
_ = erlang:cancel_timer(TRef, []),
40+
OK;
41+
{'DOWN', MRef6, _Type, _Pid, _Info} ->
42+
_ = erlang:cancel_timer(TRef, []),
43+
{Pid4, MRef4} = spawn_monitor(fun() -> try_connect(IPv4Addrs, Port, Opts, Self) end),
44+
do_connect_2(Pid4, MRef4, Timeout);
45+
{timeout, TRef, try_ipv4} ->
46+
PidRef4 = spawn_monitor(fun() -> try_connect(IPv4Addrs, Port, Opts, Self) end),
47+
do_connect_1(PidRef4, {Pid6, MRef6}, Timeout)
48+
after Timeout ->
49+
_ = erlang:cancel_timer(TRef, []),
50+
erlang:demonitor(MRef6, [flush]),
51+
{error, connect_timeout}
52+
end.
53+
54+
55+
do_connect_1({Pid4, MRef4}, {Pid6, MRef6}, Timeout) ->
56+
receive
57+
{'DOWN', MRef4, _Type, _Pid, {happy_connect, OK}} ->
58+
?report_trace("happy_connect ~p", [OK]),
59+
connect_gc(Pid6, MRef6),
60+
OK;
61+
{'DOWN', MRef4, _Type, _Pid, _Info} ->
62+
do_connect_2(Pid6, MRef6, Timeout);
63+
{'DOWN', MRef6, _Type, _Pid, {happy_connect, OK}} ->
64+
?report_trace("happy_connect ~p", [OK]),
65+
connect_gc(Pid4, MRef4),
66+
OK;
67+
{'DOWN', MRef6, _Type, Pid, _Info} ->
68+
do_connect_2(Pid4, MRef4, Timeout)
69+
after Timeout ->
70+
connect_gc(Pid4, MRef4),
71+
connect_gc(Pid6, MRef6),
72+
{error, connect_timeout}
73+
end.
74+
75+
do_connect_2(Pid, MRef, Timeout) ->
76+
receive
77+
{'DOWN', MRef, _Type, _Pid, {happy_connect, OK}} ->
78+
?report_trace("happy_connect ~p", [OK]),
79+
OK;
80+
{'DOWN', MRef, _Type, _Pid, Info} ->
81+
{connect_error, Info}
82+
after Timeout ->
83+
connect_gc(Pid, MRef),
84+
{error, connect_timeout}
85+
end.
86+
87+
connect_gc(Pid, MRef) ->
88+
catch exit(Pid, normal),
89+
erlang:demonitor(MRef, [flush]).
90+
91+
92+
-spec parse_address(inet:ip_address() | binary() | string()) -> inet:ip_address() | string().
93+
parse_address(IPTuple) when is_tuple(IPTuple) -> IPTuple;
94+
parse_address(IPBin) when is_binary(IPBin) ->
95+
parse_address(binary_to_list(IPBin));
96+
%% IPv6 string with brackets
97+
parse_address("[" ++ IPString) ->
98+
parse_address(lists:sublist(IPString, length(IPString) - 1));
99+
parse_address(IPString) ->
100+
case inet:parse_address(IPString) of
101+
{ok, IP} -> IP;
102+
{error, _} -> IPString
103+
end.
104+
105+
-spec getaddrs(string()) -> {[{inet:ip_address(), 'inet6' | 'inet'}], [{inet:ip_address(), 'inet6' | 'inet'}]}.
106+
getaddrs("localhost") ->
107+
{[{{0,0,0,0,0,0,0,1}, 'inet6'}], [{{127,0,0,1}, 'inet'}]};
108+
getaddrs(Name) ->
109+
IP6Addrs = [{Addr, 'inet6'} || Addr <- getbyname(Name, 'aaaa')],
110+
IP4Addrs = [{Addr, 'inet'} || Addr <- getbyname(Name, 'a')],
111+
{IP6Addrs, IP4Addrs}.
112+
113+
getbyname(Hostname, Type) ->
114+
case (catch inet_res:getbyname(Hostname, Type)) of
115+
{'ok', #hostent{h_addr_list=AddrList}} -> lists:usort(AddrList);
116+
{error, _Reason} -> [];
117+
Else ->
118+
%% ERLANG 22 has an issue when g matching somee DNS server messages
119+
?report_debug("DNS error", [{hostname, Hostname}
120+
,{type, Type}
121+
,{error, Else}]),
122+
[]
123+
end.
124+
125+
try_connect(Ipv6Addrs, Port, Opts, Self) ->
126+
try_connect(Ipv6Addrs, Port, Opts, Self, {error, nxdomain}).
127+
128+
try_connect([], _Port, _Opts, _ServerPid, LastError) ->
129+
?report_trace("happy eyeball: failed to connect", [{error, LastError}]),
130+
exit(LastError);
131+
try_connect([{IP, Type} | Rest], Port, Opts, ServerPid, _LastError) ->
132+
?report_trace("try to connect", [{ip, IP}, {type, Type}]),
133+
case gen_tcp:connect(IP, Port, [Type | Opts], ?TIMEOUT) of
134+
{ok, Socket} = OK ->
135+
?report_trace("success to connect", [{ip, IP}, {type, Type}]),
136+
ok = gen_tcp:controlling_process(Socket, ServerPid),
137+
exit({happy_connect, OK});
138+
Error ->
139+
try_connect(Rest, Port, Opts, ServerPid, Error)
140+
end.

src/hackney_http_connect.erl

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ connect(ProxyHost, ProxyPort, Opts, Timeout)
6363
ConnectOpts = hackney_util:filter_options(Opts, AcceptedOpts, BaseOpts),
6464

6565
%% connect to the proxy, and upgrade the socket if needed.
66-
case gen_tcp:connect(ProxyHost, ProxyPort, ConnectOpts) of
66+
case hackney_happy:connect(ProxyHost, ProxyPort, ConnectOpts) of
6767
{ok, Socket} ->
6868
case do_handshake(Socket, Host, Port, Opts) of
6969
ok ->

0 commit comments

Comments
 (0)