Permalink
Browse files

Add a nslookup_pref global parameter

This parameter allows to change the name resolution and connection
preference for fcgi servers and revproxy URLs. It takes a list of the
form [inet | inet6].

For instance, to perform only IPv4 resolution:
    nslookup_pref = [inet]

To perform both IPv4 and IPv6, but try IPv6 first:
    nslookup_pref = [inet6, inet]

The default value is [inet]. Therefore, the behavior remains the same
with this addition.

This parameter is used by two new functions:
    o  yaws:tcp_connect/{3,4}
    o  yaws:ssl_connect/{3,4}

They are wrappers around gen_tcp:connect/{3,4} and ssl:connect/{3,4} and
take care of connection retries for each configured family.
  • Loading branch information...
dumbbell committed Apr 9, 2014
1 parent 2e662ff commit 53fdbfed12648942324400074b0b7bcb5def8d2c
Showing with 212 additions and 19 deletions.
  1. +7 −0 doc/yaws.tex
  2. +2 −1 include/yaws.hrl
  3. +7 −0 man/yaws.conf.5
  4. +114 −2 src/yaws.erl
  5. +14 −9 src/yaws_cgi.erl
  6. +47 −0 src/yaws_config.erl
  7. +3 −7 src/yaws_revproxy.erl
  8. +9 −0 test/conf/revproxyconf.conf
  9. +9 −0 test/t4/app_test.erl
View
@@ -3118,6 +3118,13 @@ \section{Server Part}
in a server part using this directive, then it is replaced. Else
it is kept.
+\item \verb+nslookup_pref = [inet | inet6]+ ---
+ For fcgi servers and revproxy URLs, define the name
+ resolution preference. For example, to perform only IPv4
+ name resolution, use \textit{[inet]}. To do both IPv4
+ and IPv6 but try IPv6 first, use \textit{[inet6, inet]}.
+ Default value is \textit{[inet]}.
+
\item \verb+<ssl> .... </ssl>+
This begins and ends an SSL configuration for this server. It's
possible to virthost several SSL servers on the same IP given
View
@@ -109,7 +109,8 @@
ysession_mod = yaws_session_server, % storage module for ysession
acceptor_pool_size = 8, % size of acceptor proc pool
- mime_types_info % undefined | #mime_types_info{}
+ mime_types_info, % undefined | #mime_types_info{}
+ nslookup_pref = [inet] % [inet | inet6]
}).
-record(ssl, {
View
@@ -981,6 +981,13 @@ Overloads the global \fBadd_charsets\fR values for this virtual server. If a
mapping is defined in the global part and redefined in a server part using this
directive, then it is replaced. Else it is kept.
+.TP
+\fBnslookup_pref = [inet | inet6]\fR
+For fcgi servers and revproxy URLs, define the name resolution
+preference. For example, to perform only IPv4 name resolution, use
+[inet]. To do both IPv4 and IPv6 but try IPv6 first, use [inet6, inet].
+Default value is [inet].
+
.TP
\fB<ssl> ... </ssl>\fR
This begins and ends an SSL configuration for this server. It's possible to
View
@@ -26,7 +26,8 @@
gconf_mnesia_dir/1, gconf_log_wrap_size/1, gconf_cache_refresh_secs/1,
gconf_include_dir/1, gconf_phpexe/1, gconf_yaws/1, gconf_id/1,
gconf_enable_soap/1, gconf_soap_srv_mods/1, gconf_ysession_mod/1,
- gconf_acceptor_pool_size/1, gconf_mime_types_info/1]).
+ gconf_acceptor_pool_size/1, gconf_mime_types_info/1,
+ gconf_nslookup_pref/1]).
-export([sconf_port/1, sconf_flags/1, sconf_redirect_map/1, sconf_rhost/1,
sconf_rmethod/1, sconf_docroot/1, sconf_xtra_docroots/1,
@@ -146,6 +147,8 @@
exists/1,
mkdir/1]).
+-export([tcp_connect/3, tcp_connect/4, ssl_connect/3, ssl_connect/4]).
+
-export([do_recv/3, do_recv/4, cli_recv/3,
gen_tcp_send/2,
http_get_headers/2]).
@@ -239,6 +242,7 @@ gconf_soap_srv_mods (#gconf{soap_srv_mods = X}) -> X.
gconf_ysession_mod (#gconf{ysession_mod = X}) -> X.
gconf_acceptor_pool_size (#gconf{acceptor_pool_size = X}) -> X.
gconf_mime_types_info (#gconf{mime_types_info = X}) -> X.
+gconf_nslookup_pref (#gconf{nslookup_pref = X}) -> X.
sconf_port (#sconf{port = X}) -> X.
@@ -518,7 +522,9 @@ setup_gconf(GL, GC) ->
GC#gconf.acceptor_pool_size),
mime_types_info = setup_mime_types_info(
GL, GC#gconf.mime_types_info
- )
+ ),
+ nslookup_pref = lkup(nslookup_pref, GL,
+ GC#gconf.nslookup_pref)
}.
set_gc_flags([{tty_trace, Bool}|T], Flags) ->
@@ -1974,6 +1980,112 @@ ensure_exist(Path) ->
end
end.
+%%
+%%
+%% TCP/SSL connection with a configurable IPv4/IPv6 preference on NS lookup.
+%%
+%%
+
+tcp_connect(Host, Port, Options) ->
+ tcp_connect(Host, Port, Options, infinity).
+
+tcp_connect(Host, Port, Options, Timeout) ->
+ parse_ipaddr_and_connect(tcp, Host, Port, Options, Timeout).
+
+ssl_connect(Host, Port, Options) ->
+ ssl_connect(Host, Port, Options, infinity).
+
+ssl_connect(Host, Port, Options, Timeout) ->
+ parse_ipaddr_and_connect(ssl, Host, Port, Options, Timeout).
+
+parse_ipaddr_and_connect(Proto, IP, Port, Options, Timeout)
+when is_tuple(IP) ->
+ %% The caller handled name resolution himself.
+ filter_tcpoptions_and_connect(Proto, undefined,
+ IP, Port, Options, Timeout);
+parse_ipaddr_and_connect(Proto, [$[ | Rest], Port, Options, Timeout) ->
+ %% yaws_api:parse_url/1 keep the "[...]" enclosing an IPv6 address.
+ %% Remove them now, and parse the address.
+ IP = string:strip(Rest, right, $]),
+ parse_ipaddr_and_connect(Proto, IP, Port, Options, Timeout);
+parse_ipaddr_and_connect(Proto, Host, Port, Options, Timeout) ->
+ %% First, try to parse an IP address, because inet:getaddr/2 could
+ %% return nxdomain if the family doesn't match the IP address
+ %% format.
+ case inet:parse_strict_address(Host) of
+ {ok, IP} ->
+ filter_tcpoptions_and_connect(Proto, undefined,
+ IP, Port, Options, Timeout);
+ {error, einval} ->
+ NsLookupPref = get_nslookup_pref(Options),
+ filter_tcpoptions_and_connect(Proto, NsLookupPref,
+ Host, Port, Options, Timeout)
+ end.
+
+filter_tcpoptions_and_connect(Proto, NsLookupPref,
+ Host, Port, Options, Timeout) ->
+ %% Now that we have IP addresses, remove family from the TCP options,
+ %% because calling gen_tcp:connect/3 with {127,0,0,1} and [inet6]
+ %% would return {error, nxdomain otherwise}.
+ OptionsWithoutFamily = lists:filter(fun
+ (inet) -> false;
+ (inet6) -> false;
+ (_) -> true
+ end, Options),
+ resolve_and_connect(Proto, NsLookupPref, Host, Port, OptionsWithoutFamily, Timeout).
+
+resolve_and_connect(Proto, _, IP, Port, Options, Timeout)
+when is_tuple(IP) ->
+ do_connect(Proto, IP, Port, Options, Timeout);
+resolve_and_connect(Proto, [Family | Rest], Host, Port, Options, Timeout) ->
+ Result = case inet:getaddr(Host, Family) of
+ {ok, IP} -> do_connect(Proto, IP, Port, Options, Timeout);
+ R -> R
+ end,
+ case Result of
+ {ok, Socket} ->
+ {ok, Socket};
+ {error, _} when length(Rest) >= 1 ->
+ %% If the connection fails here, ignore the error and
+ %% continue with the next address family.
+ resolve_and_connect(Proto, Rest, Host, Port, Options, Timeout);
+ {error, Reason} ->
+ %% This was the last IP address in the list, return the
+ %% connection error.
+ {error, Reason}
+ end.
+
+do_connect(Proto, IP, Port, Options, Timeout) ->
+ case Proto of
+ tcp -> gen_tcp:connect(IP, Port, Options, Timeout);
+ ssl -> ssl:connect(IP, Port, Options, Timeout)
+ end.
+
+%% If the caller specified inet or inet6 in the TCP options, prefer
+%% this to the global nslookup_pref parameter.
+%%
+%% This can be used in processes which can't use get(gc) to get the
+%% global conf: if they are given the global conf, they can get
+%% nslookup_pref value and add it the TCP options.
+%%
+%% If neither TCP options specify the family, nor the global conf is
+%% accessible, use default value declared in #gconf definition.
+get_nslookup_pref(TcpOptions) ->
+ get_nslookup_pref(TcpOptions, []).
+
+get_nslookup_pref([inet | Rest], Result) ->
+ get_nslookup_pref(Rest, [inet | Result]);
+get_nslookup_pref([inet6 | Rest], Result) ->
+ get_nslookup_pref(Rest, [inet6 | Result]);
+get_nslookup_pref([_ | Rest], Result) ->
+ get_nslookup_pref(Rest, Result);
+get_nslookup_pref([], []) ->
+ case get(gc) of
+ undefined -> gconf_nslookup_pref(#gconf{});
+ GC -> gconf_nslookup_pref(GC)
+ end;
+get_nslookup_pref([], Result) ->
+ lists:reverse(Result).
%%
%%
View
@@ -21,7 +21,7 @@
%%% TODO: Implement FastCGI filter role.
--export([cgi_worker/7, fcgi_worker/5]).
+-export([cgi_worker/7, fcgi_worker/6]).
%%%=====================================================================
%%% Code shared between CGI and FastCGI
@@ -802,8 +802,10 @@ call_fcgi(Role, Arg, Options) ->
[Role, fcgi_role_name(Role),
Options,
Arg]),
+ GlobalConf = get(gc),
ServerConf = get(sc),
- WorkerPid = fcgi_start_worker(Role, Arg, ServerConf, Options),
+ WorkerPid = fcgi_start_worker(Role, Arg, GlobalConf, ServerConf,
+ Options),
handle_clidata(Arg, WorkerPid)
end.
@@ -829,12 +831,12 @@ fcgi_worker_fail_if(true, WorkerState, Reason) ->
fcgi_worker_fail_if(_Condition, _WorkerState, _Reason) ->
ok.
-fcgi_start_worker(Role, Arg, ServerConf, Options) ->
+fcgi_start_worker(Role, Arg, GlobalConf, ServerConf, Options) ->
proc_lib:spawn(?MODULE, fcgi_worker,
- [self(), Role, Arg, ServerConf, Options]).
+ [self(), Role, Arg, GlobalConf, ServerConf, Options]).
-fcgi_worker(ParentPid, Role, Arg, ServerConf, Options) ->
+fcgi_worker(ParentPid, Role, Arg, GlobalConf, ServerConf, Options) ->
{DefaultSvrHost, DefaultSvrPort} =
case ServerConf#sconf.fcgi_app_server of
undefined ->
@@ -857,9 +859,11 @@ fcgi_worker(ParentPid, Role, Arg, ServerConf, Options) ->
?sc_fcgi_trace_protocol(ServerConf)),
LogAppError = get_opt(log_app_error, Options,
?sc_fcgi_log_app_error(ServerConf)),
+ TcpOptions = yaws:gconf_nslookup_pref(GlobalConf),
AppServerSocket =
fcgi_connect_to_application_server(PreliminaryWorkerState,
- AppServerHost, AppServerPort),
+ AppServerHost, AppServerPort,
+ TcpOptions),
?Debug("Start FastCGI worker:~n"
" Role = ~p (~s)~n"
" AppServerHost = ~p~n"
@@ -910,9 +914,10 @@ fcgi_pass_through_client_data(WorkerState) ->
end.
-fcgi_connect_to_application_server(WorkerState, Host, Port) ->
- Options = [binary, {packet, 0}, {active, false}, {nodelay, true}],
- case gen_tcp:connect(Host, Port, Options, ?FCGI_CONNECT_TIMEOUT_MSECS) of
+fcgi_connect_to_application_server(WorkerState, Host, Port, TcpOptions) ->
+ Options = [binary, {packet, 0}, {active, false}, {nodelay, true} |
+ TcpOptions],
+ case yaws:tcp_connect(Host, Port, Options, ?FCGI_CONNECT_TIMEOUT_MSECS) of
{error, Reason} ->
fcgi_worker_fail(WorkerState,
{"connect to application server failed", Reason});
View
@@ -1130,6 +1130,14 @@ fload(FD, globals, GC, C, Cs, Lno, Chars) ->
{error, Str} ->
{error, ?F("~s at line ~w", [Str, Lno])}
end;
+ ["nslookup_pref", '=' | Pref] ->
+ case parse_nslookup_pref(Pref) of
+ {ok, Families} ->
+ fload(FD, globals, GC#gconf{nslookup_pref = Families},
+ C, Cs, Lno+1, Next);
+ {error, Str} ->
+ {error, ?F("~s at line ~w", [Str, Lno])}
+ end;
['<', "server", Server, '>'] -> %% first server
@@ -2597,6 +2605,45 @@ parse_mime_types_info(add_charsets, NewCharsets, Info) ->
end.
+parse_nslookup_pref(Pref) ->
+ parse_nslookup_pref(Pref, []).
+
+parse_nslookup_pref(Empty, []) when Empty == [] orelse Empty == ['[', ']'] ->
+ %% Get default value, if nslookup_pref = [].
+ {ok, yaws:gconf_nslookup_pref(#gconf{})};
+parse_nslookup_pref([C, Family | Rest], Result)
+ when C == '[' orelse C == ',' ->
+ case Family of
+ "inet" ->
+ case lists:member(inet, Result) of
+ false -> parse_nslookup_pref(Rest, [inet | Result]);
+ true -> parse_nslookup_pref(Rest, Result)
+ end;
+ "inet6" ->
+ case lists:member(inet6, Result) of
+ false -> parse_nslookup_pref(Rest, [inet6 | Result]);
+ true -> parse_nslookup_pref(Rest, Result)
+ end;
+ _ ->
+ case Result of
+ [PreviousFamily | _] ->
+ {error, ?F("Invalid nslookup_pref: invalid family or "
+ "token '~s', after family '~s'",
+ [Family, PreviousFamily])};
+ [] ->
+ {error, ?F("Invalid nslookup_pref: invalid family or "
+ "token '~s'", [Family])}
+ end
+ end;
+parse_nslookup_pref([']'], Result) ->
+ {ok, lists:reverse(Result)};
+parse_nslookup_pref([Invalid | _], []) ->
+ {error, ?F("Invalid nslookup_pref: unexpected token '~s'", [Invalid])};
+parse_nslookup_pref([Invalid | _], [Family | _]) ->
+ {error, ?F("Invalid nslookup_pref: unexpected token '~s', "
+ "after family '~s'", [Invalid, Family])}.
+
+
parse_redirect(Path, [Code, URL], Mode, Lno) ->
case catch list_to_integer(Code) of
I when is_integer(I), I >= 300, I =< 399 ->
View
@@ -574,24 +574,20 @@ connect(URL) ->
end.
do_connect(URL) ->
- InetType = if
- is_tuple(URL#url.host), size(URL#url.host) == 8 -> [inet6];
- true -> []
- end,
Opts = [
binary,
{packet, raw},
{active, false},
{recbuf, 8192},
{reuseaddr, true}
- ] ++ InetType,
+ ],
case URL#url.scheme of
http ->
Port = case URL#url.port of
undefined -> 80;
P -> P
end,
- case gen_tcp:connect(URL#url.host, Port, Opts) of
+ case yaws:tcp_connect(URL#url.host, Port, Opts) of
{ok, S} -> {ok, S, nossl};
Err -> Err
end;
@@ -600,7 +596,7 @@ do_connect(URL) ->
undefined -> 443;
P -> P
end,
- case ssl:connect(URL#url.host, Port, Opts) of
+ case yaws:ssl_connect(URL#url.host, Port, Opts) of
{ok, S} -> {ok, S, ssl};
Err -> Err
end;
@@ -110,6 +110,7 @@ use_fdsrv = false
arg_rewrite_mod = rewritetest
revproxy = /revproxy1 http://localhost:8001
revproxy = /revproxy2 http://localhost:8002
+ revproxy = /revproxy3 http://[::1]:8006
</server>
<server localhost>
@@ -157,3 +158,11 @@ use_fdsrv = false
revproxy = /revproxy2 http://localhost:8002 intercept_mod intercept2
revproxy = /revproxy3 http://localhost:8002 intercept_mod intercept3
</server>
+
+<server localhost>
+ port = 8006
+ listen = ::
+ listen_backlog = 512
+ deflate = false
+ docroot = %YTOP%/test/t4/www1
+</server>
View
@@ -33,6 +33,7 @@ start() ->
test_failed_resp_interception_revproxy(),
test_good_interception_revproxy(),
test_fwdproxy(),
+ test_ipv6_address(),
ok
catch
Error:Reason ->
@@ -257,6 +258,14 @@ test_fwdproxy() ->
?line Res = Body2,
ok.
+test_ipv6_address() ->
+ io:format("revproxy_url_ipv6\n", []),
+ Uri = "http://localhost:8000/revproxy3/hello.txt",
+ Res = "Hello, World!\n",
+
+ ?line {ok, "200", _, _} = ibrowse:send_req(Uri, [], get),
+ ok.
+
recv_hdrs(Sock) ->
inet:setopts(Sock, [{packet, http}]),
recv_hdrs(Sock, 0).

0 comments on commit 53fdbfe

Please sign in to comment.