Skip to content

Commit

Permalink
Add a nslookup_pref global parameter
Browse files Browse the repository at this point in the history
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 15, 2014
1 parent 2e662ff commit 53fdbfe
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 19 deletions.
7 changes: 7 additions & 0 deletions doc/yaws.tex
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion include/yaws.hrl
Expand Up @@ -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, {
Expand Down
7 changes: 7 additions & 0 deletions man/yaws.conf.5
Expand Up @@ -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
Expand Down
116 changes: 114 additions & 2 deletions src/yaws.erl
Expand Up @@ -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,
Expand Down Expand Up @@ -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]).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) ->
Expand Down Expand Up @@ -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).

%%
%%
Expand Down
23 changes: 14 additions & 9 deletions src/yaws_cgi.erl
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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 ->
Expand All @@ -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"
Expand Down Expand Up @@ -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});
Expand Down
47 changes: 47 additions & 0 deletions src/yaws_config.erl
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down
10 changes: 3 additions & 7 deletions src/yaws_revproxy.erl
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions test/conf/revproxyconf.conf
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
9 changes: 9 additions & 0 deletions test/t4/app_test.erl
Expand Up @@ -33,6 +33,7 @@ start() ->
test_failed_resp_interception_revproxy(),
test_good_interception_revproxy(),
test_fwdproxy(),
test_ipv6_address(),
ok
catch
Error:Reason ->
Expand Down Expand Up @@ -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).
Expand Down

0 comments on commit 53fdbfe

Please sign in to comment.