Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1357 lines (1178 sloc) 49.736 kb
-module(yaws_cgi).
-author('carsten@codimi.de').
-author('brunorijsman@hotmail.com'). %% Added support for FastCGI
-include("../include/yaws_api.hrl").
-include("yaws_debug.hrl").
-include("../include/yaws.hrl").
%%% Returns Out (i.e. same return values as out/1).
%%%
-export([call_cgi/5, call_cgi/4, call_cgi/3, call_cgi/2]).
-export([call_fcgi_responder/2, call_fcgi_responder/1]).
%%% Returns {allowed, Out} or {denied, Out}.
%%%
-export([call_fcgi_authorizer/2, call_fcgi_authorizer/1]).
%%% Returns [{VariableName, VariableValue}].
%%%
-export([fcgi_extract_variables/1]).
%%% TODO: Implement FastCGI filter role.
-export([cgi_worker/7, fcgi_worker/5]).
%%%==============================================================================
%%% Code shared between CGI and FastCGI
%%%==============================================================================
-define(ASCII_NEW_LINE, 10).
-define(ASCII_CARRIAGE_RETURN, 13).
handle_clidata(Arg, WorkerPid) ->
case Arg#arg.clidata of
undefined ->
end_of_clidata(Arg, WorkerPid);
{partial, Data} ->
send_clidata(WorkerPid, Data),
{get_more, cgicont, {cgistate, WorkerPid}};
Data when is_binary(Data) ->
send_clidata(WorkerPid, Data),
end_of_clidata(Arg, WorkerPid)
end.
end_of_clidata(Arg, WorkerPid) ->
WorkerPid ! {self(), end_of_clidata},
get_from_worker(Arg, WorkerPid).
send_clidata(WorkerPid, Data) ->
WorkerPid ! {self(), clidata, Data},
receive
{WorkerPid, clidata_receipt} -> ok
end.
get_from_worker(Arg, WorkerPid) ->
case get_resp(WorkerPid) of
{failure, Reason} ->
[{status, 500}, {html, io_lib:format("CGI failure: ~p", [Reason])}];
{Headers, Data} ->
AllResps = lists:map(fun(X)-> do_header(Arg, X, Data) end, Headers),
%%
%% The CGI 1.1 spec (RFC 3875) requires a worker response
%% consisting of only a location header and optional extension
%% headers to be augmented with a 302 status code. Any other
%% worker response with a location header is handled normally.
%% Technically a response of the latter type MUST have a status
%% code in it, but we don't enforce that.
%%
{LocHdr, _ExtHdrs, TheRest} =
lists:foldl(
fun({header, Line}=Hdr, {Loc, Ext, Rest}) ->
{HdrLower, _HdrVal} = do_lower_header(Line),
case HdrLower of
"location" ->
{[Hdr], Ext, Rest};
"x-cgi-"++_ ->
{Loc, [Hdr|Ext], Rest};
_ ->
{Loc, Ext, [Hdr|Rest]}
end;
(Hdr, {Loc, Ext, Rest}) ->
{Loc, Ext, [Hdr|Rest]}
end, {[], [], []}, AllResps),
Next = case LocHdr of
[] ->
normal;
[{header, _Location}] ->
case TheRest of
[] ->
location_add_302;
_ ->
normal
end
end,
case Next of
normal ->
{ContentResps, NotCtnt} = filter2(fun iscontent/1, AllResps),
{RedirResps, Others} = filter2(fun isredirect/1, NotCtnt),
case RedirResps of
[R|_] ->
WorkerPid ! {self(), no_data},
Others ++ [R];
[] ->
case ContentResps of
[C={streamcontent, _, _}|_] ->
WorkerPid ! {self(), stream_data},
Others++[C];
[C={content, _, _}|_] ->
WorkerPid ! {self(), no_data},
Others++[C];
[] ->
WorkerPid ! {self(), no_data},
Others
end
end;
location_add_302 ->
WorkerPid ! {self(), no_data},
AllResps++[{status, 302}]
end
end.
filter2(Pred, Xs) ->
filter2(Pred, Xs, [], []).
filter2(_Pred, [], Ts, Fs) ->
{lists:reverse(Ts), lists:reverse(Fs)};
filter2(Pred, [X|Xs], Ts, Fs) ->
case Pred(X) of
true ->
filter2(Pred, Xs, [X|Ts], Fs);
false ->
filter2(Pred, Xs, Ts, [X|Fs])
end.
iscontent({content, _, _}) ->
true;
iscontent({streamcontent, _, _}) ->
true;
iscontent(_) ->
false.
isredirect({status, I}) when is_integer(I) , I > 301, I < 304; I =:= 307 ->
true;
isredirect(_) ->
false.
checkdef(undefined) ->
"";
checkdef(L) ->
L.
deep_drop_prefix([], L) ->
L;
deep_drop_prefix([X|Xs], [X|Ys]) when is_integer(X) ->
deep_drop_prefix(Xs, Ys);
deep_drop_prefix([X|Xs], Ys) when is_list(X) ->
deep_drop_prefix(X++Xs, Ys);
deep_drop_prefix(Xs, [Y|Ys]) when is_list(Y) ->
deep_drop_prefix(Xs, Y++Ys);
deep_drop_prefix(_, _) ->
false.
get_socket_peername({ssl, SslSocket}) ->
{ok, {IP, _Port}}=ssl:peername(SslSocket),
inet_parse:ntoa(IP);
get_socket_peername(Socket) ->
{ok, {IP, _Port}}=inet:peername(Socket),
inet_parse:ntoa(IP).
get_socket_sockname({ssl, SslSocket}) ->
{ok, {IP, _Port}}=ssl:sockname(SslSocket),
inet_parse:ntoa(IP);
get_socket_sockname(Socket) ->
{ok, {IP, _Port}}=inet:sockname(Socket),
inet_parse:ntoa(IP).
build_env(Arg, Scriptfilename, Pathinfo, ExtraEnv, SC) ->
H = Arg#arg.headers,
R = Arg#arg.req,
case R#http_request.path of
{abs_path, RequestURI} -> ok;
_ -> RequestURI = undefined
end,
{Maj,Min} = R#http_request.version,
{Hostname, Hosttail}=lists:splitwith(fun(X)->X /= $: end,
checkdef(H#headers.host)),
Hostport = case Hosttail of
[$: | P] -> P;
[] -> integer_to_list(SC#sconf.port)
end,
PeerAddr = get_socket_peername(Arg#arg.clisock),
LocalAddr = get_socket_sockname(Arg#arg.clisock),
Scheme = (catch yaws:redirect_scheme(SC)),
%% Needed by trac, for redirs after POST
HttpsEnv = case Scheme of
"https://" -> [{"HTTPS", "1"}];
_ ->[]
end,
%%Scriptname = deep_drop_prefix(Arg#arg.docroot, Arg#arg.fullpath),
%%SCRIPT_NAME is the path of the script relative to the root of the website.
%%just dropping docroot from the fullpath does not give the full SCRIPT_NAME
%% path if a 'vdir' is involved.
UriTail = deep_drop_prefix(Arg#arg.docroot, Arg#arg.fullpath),
case Arg#arg.docroot_mount of
"/" ->
%%no arg.docroot_mount means that arg.docroot
%% corresponds to the URI-root of the request "/"
Scriptname = UriTail;
Vdir ->
Scriptname = Vdir ++ string:strip(UriTail,left,$/)
end,
Pathinfo2 = checkdef(Pathinfo),
case Pathinfo2 of
"" ->
PathTranslated = "";
_ ->
%%determine what physical path the server would map Pathinfo2
%%to if it had received just Pathinfo2 in the request.
PathTranslated = yaws_server:mappath(SC,Arg,Pathinfo2)
end,
%%Pass auth info in environment - yes - including password in plain text.
%%REMOTE_USER always = AUTH_USER
%%!todo - LOGON_USER - same as AUTH_USER unless some auth filter has mapped
%%the user to another username under which to run the request.
case H#headers.authorization of
undefined ->
AuthEnv = [];
{undefined, _, _} ->
AuthEnv = [];
{User, Password, "Basic " ++ Auth64} ->
AuthEnv = [
{"HTTP_AUTHORIZATION", "Basic " ++ Auth64},
{"AUTH_TYPE", "Basic"},
{"AUTH_USER", User},
{"REMOTE_USER", User},
{"LOGON_USER", User},
{"AUTH_PASSWORD", Password}
];
{_User, _Password, _OrigString} ->
%%not attempting to pass through any auth info for
%% auth schemes that we don't yet handle
AuthEnv = []
end,
Extra_CGI_Vars = lists:flatmap(fun({Dir, Vars}) ->
case lists:prefix(Dir, Scriptname) of
true -> Vars;
false -> []
end
end,
SC#sconf.extra_cgi_vars),
%% Some versions of erlang:open_port can't handle query strings that
%% end with an equal sign. This is because the broken versions treat
%% environment variable strings ending with '=' as environment variable
%% names intended to be deleted from the environment, i.e. as if they
%% have no value. The result is that no QUERY_STRING environment
%% variable gets set for these cases. We work around this by appending
%% a & character to any query string that ends in =.
QueryString = case checkdef(Arg#arg.querydata) of
"" ->
"";
QS ->
case lists:reverse(QS) of
[$= | _] ->
QS ++ "&";
_ ->
QS
end
end,
%%todo - review. should AuthEnv entries be overridable by ExtraEnv or not?
%% we should define policy here rather than let through dupes.
ExtraEnv ++
HttpsEnv ++
AuthEnv ++
lists:filter(
fun({K, L}) when is_list(L) ->
case lists:keysearch(K, 1, ExtraEnv) of
false ->
true;
_ ->
%% we have override in extraenv
false
end;
(_) ->
false
end,
([
{"SERVER_SOFTWARE", "Yaws/"++yaws_generated:version()},
{"SERVER_NAME", Hostname},
{"HTTP_HOST", checkdef(H#headers.host)},
{"GATEWAY_INTERFACE", "CGI/1.1"},
{"SERVER_PROTOCOL", "HTTP/" ++ integer_to_list(Maj) ++
"." ++ integer_to_list(Min)},
{"SERVER_PORT", Hostport},
{"REQUEST_METHOD", yaws:to_list(R#http_request.method)},
{"REQUEST_URI", RequestURI},
{"DOCUMENT_ROOT", Arg#arg.docroot},
{"DOCUMENT_ROOT_MOUNT", Arg#arg.docroot_mount},
%% SCRIPT_FILENAME is for PHP 4.3.2 and higher
%% see http://bugs.php.net/bug.php?id=28227
%% (Sergei Golovan).
{"SCRIPT_FILENAME", Scriptfilename},
%% {"SCRIPT_TRANSLATED", Scriptfilename}, %IIS6+
{"PATH_INFO", Pathinfo2},
{"PATH_TRANSLATED", PathTranslated},
%% <JMN_2007-02>
%% CGI/1.1 spec says PATH_TRANSLATED should be NULL or unset
%% if PATH_INFO is NULL
%% This is in contrast to IIS behaviour - and may break some apps.
%% broken apps that expect it to always correspond to path of
%% script
%% should be modified to use SCRIPT_FILENAME instead - or wrapped.
%% </JMN_2007-02>
%% --------------------
%% <pre_2007-02_comments>
%% This seems not to
%% correspond to the
%% documentation I have
%% read, but it works
%% with PHP.
%%
%% (Not with PHP 4.3.10-16) from
%% Debian sarge (Sergei Golovan).
%% </pre_2007-02_comments>
%% ---------------------
{"SCRIPT_NAME", Scriptname},
{"REMOTE_ADDR", PeerAddr},
{"REMOTE_HOST", PeerAddr}, %% We SHOULD send this
%% Resolving DNS not practical for performance reasons
%% - at least on 1st contact from a particular host.
%% we could do background lookup so that it's available
%% for subsequent invocations,
%% but it hardly seems worthwhile. We are permitted by the
%% CGI/1.1 spec to substitute REMOTE_ADDR
{"SERVER_ADDR", LocalAddr}, %% Apache compat
{"LOCAL_ADDR", LocalAddr}, %% IIS compat
{"QUERY_STRING", QueryString},
{"CONTENT_TYPE", H#headers.content_type},
{"CONTENT_LENGTH", H#headers.content_length},
{"HTTP_ACCEPT", H#headers.accept},
{"HTTP_USER_AGENT", H#headers.user_agent},
{"HTTP_REFERER", H#headers.referer},
{"HTTP_IF_MODIFIED_SINCE", H#headers.if_modified_since},
{"HTTP_IF_MATCH", H#headers.if_match},
{"HTTP_IF_NONE_MATCH", H#headers.if_none_match},
{"HTTP_IF_UNMODIFIED_SINCE", H#headers.if_unmodified_since},
{"HTTP_COOKIE", flatten_val(make_cookie_val(H#headers.cookie))}
]++lists:map(fun({http_header,_,Var,_,Val})->{tohttp(Var),Val} end,
H#headers.other)
)) ++
Extra_CGI_Vars.
tohttp(X) ->
"HTTP_"++lists:map(fun tohttp_c/1, yaws:to_list(X)).
tohttp_c($-) ->
$_;
tohttp_c(C) when C >= $a , C =< $z ->
C - $a + $A;
tohttp_c(C) ->
C.
make_cookie_val([]) ->
undefined;
make_cookie_val([C]) ->
C;
make_cookie_val([C|CS]) ->
[make_cookie_val(CS), $; | C].
%%% Seems not to be necessary, but open_port documentation says that
%%% value has to be a string.
flatten_val(L) when is_list(L) ->
lists:flatten(L);
flatten_val(X) ->
X.
notslash($/) ->
false;
notslash(_) ->
true.
pathof(F) ->
case lists:dropwhile(fun notslash/1, lists:reverse(F)) of
"/" ->
"/";
[$/ | Tail] -> lists:reverse(Tail)
end.
exeof(F) ->
[$\., $/|lists:reverse(lists:takewhile(fun notslash/1, lists:reverse(F)))].
do_header(_Arg, "HTTP/1."++[_,_,N1,N2,N3|_], _) ->
{status, list_to_integer([N1,N2,N3])};
do_header(Arg, Header, Data) when is_list(Header) ->
{HdrLower, HdrVal} = do_lower_header(Header),
do_header(Arg, {HdrLower, yaws:join_sep(HdrVal, ":"), Header}, Data);
do_header(_Arg, {"content-type", CT, _}, {partial_data, Data}) ->
{streamcontent, CT, Data};
do_header(_Arg, {"content-type", CT, _}, {all_data, Data}) ->
{content, CT, Data};
do_header(_Arg, {"status", [N1,N2,N3|_], _}, _) ->
{status, list_to_integer([N1,N2,N3])};
do_header(_Arg, {_, _, Line}, _) ->
{header, Line}.
do_lower_header(Header) ->
[HdrName | HdrVal] = yaws:split_sep(Header, $:),
HdrNmParts = [yaws:to_lower(H) || H <- yaws:split_sep(HdrName, $-)],
{yaws:join_sep(HdrNmParts, "-"), HdrVal}.
get_resp(WorkerPid) ->
get_resp([], WorkerPid).
get_resp(Hs, WorkerPid) ->
receive
{WorkerPid, header, H} ->
?Debug("~p~n", [{WorkerPid, header, H}]),
get_resp([H|Hs], WorkerPid);
{WorkerPid, all_data, Data} ->
?Debug("~p~n", [{WorkerPid, all_data, Data}]),
{Hs, {all_data, Data}};
{WorkerPid, partial_data, Data} ->
?Debug("~p~n", [{WorkerPid, partial_data, binary_to_list(Data)}]),
{Hs, {partial_data, Data}};
{WorkerPid, failure, Reason} ->
?Debug("~p~n", [{WorkerPid, failure, Reason}]),
{failure, Reason};
_Other ->
?Debug("~p~n", [_Other]),
get_resp(Hs, WorkerPid)
end.
get_opt(Key, List, Default) ->
case lists:keysearch(Key, 1, List) of
{value, {_Key, Val}} -> Val;
_ -> Default
end.
%%%==============================================================================
%%% Code specific to CGI
%%%==============================================================================
%%% TO DO: Handle failure and timeouts.
%%% call_cgi calls the script `Scriptfilename' (full path).
%%% If `Exefilename' is given, it is the executable to handle this,
%%% otherwise `Scriptfilame' is assumed to be executable itself.
%%%
%%% Corresponding to a URI of
%%% `http://somehost/some/dir/script.cgi/path/info',
%%% `Pathinfo' should be set to `/path/info'.
%%% These functions can be used from a `.yaws' file.
%%% Note however, that they usually generate stream content.
call_cgi(Arg, Scriptfilename) ->
call_cgi(Arg, undefined, Scriptfilename, undefined, []).
call_cgi(Arg, Exefilename, Scriptfilename) ->
call_cgi(Arg, Exefilename, Scriptfilename, undefined, []).
call_cgi(Arg, Exefilename, Scriptfilename, Pathinfo) ->
call_cgi(Arg, Exefilename, Scriptfilename, Pathinfo, []).
call_cgi(Arg, Exefilename, Scriptfilename, Pathinfo, ExtraEnv) ->
case Arg#arg.state of
{cgistate, WorkerPid} ->
case Arg#arg.cont of
cgicont ->
handle_clidata(Arg, WorkerPid);
undefined ->
?Debug("Error while reading clidata: ~p~n",
[Arg#arg.clidata]),
%% Error, what to do?
exit(normal)
end;
_ ->
WorkerPid = cgi_start_worker(Arg, Exefilename, Scriptfilename,
Pathinfo, ExtraEnv, get(sc)),
handle_clidata(Arg, WorkerPid)
end.
cgi_start_worker(Arg, Exefilename, Scriptfilename, Pathinfo, ExtraEnv, SC) ->
ExeFN = case Exefilename of
undefined -> exeof(Scriptfilename);
"" -> exeof(Scriptfilename);
FN -> FN
end,
PI = case Pathinfo of
undefined -> Arg#arg.pathinfo;
OK -> OK
end,
WorkerPid = proc_lib:spawn(?MODULE, cgi_worker,
[self(), Arg, ExeFN, Scriptfilename,
PI, ExtraEnv, SC]),
WorkerPid.
cgi_worker(Parent, Arg, Exefilename, Scriptfilename, Pathinfo, ExtraEnv,SC) ->
Env = build_env(Arg, Scriptfilename, Pathinfo, ExtraEnv,SC),
?Debug("~p~n", [Env]),
CGIPort = open_port({spawn, Exefilename},
[{env, Env},
{cd, pathof(Scriptfilename)},
exit_status,
binary]),
cgi_pass_through_clidata(Parent, CGIPort),
cgi_do_work(Parent, Arg, CGIPort).
cgi_pass_through_clidata(Parent, CGIPort) ->
receive
{Parent, clidata, Clidata} ->
?Debug("Got clidata ~p~n", [binary_to_list(Clidata)]),
Parent ! {self(), clidata_receipt},
CGIPort ! {self(), {command, Clidata}},
cgi_pass_through_clidata(Parent, CGIPort);
{Parent, end_of_clidata} ->
?Debug("End of clidata~n", []),
ok
end.
cgi_do_work(Parent, Arg, Port) ->
cgi_header_loop(Parent, Arg, {start, Port}).
cgi_header_loop(Parent, Arg, S) ->
Line = cgi_get_line(S),
?Debug("Line = ~p~n", [Line]),
case Line of
{failure, F} ->
Parent ! {self(), failure, F};
{[], T} ->
case T of
{middle, Data, Port} ->
Parent ! {self(), partial_data, Data},
receive
{Parent, stream_data} ->
cgi_data_loop(Arg#arg.pid, Port);
{Parent, no_data} ->
ok
end;
{ending, Data, _} ->
Parent ! {self(), all_data, Data},
receive
{Parent, stream_data} ->
yaws_api:stream_chunk_end(Arg#arg.pid);
{Parent, no_data} ->
ok
end
end;
{H, T} ->
Parent ! {self(), header, H},
cgi_header_loop(Parent, Arg, T)
end.
cgi_data_loop(Pid, Port) ->
receive
{Port, {data,Data}} ->
?Debug("~p~n", [{data, binary_to_list(Data)}]),
yaws_api:stream_chunk_deliver_blocking(Pid, Data),
cgi_data_loop(Pid, Port);
{Port, {exit_status, _Status}} ->
?Debug("~p~n", [{exit_status, _Status}]),
yaws_api:stream_chunk_end(Pid);
_Other ->
?Debug("~p~n", [_Other]),
cgi_data_loop(Pid, Port)
end.
cgi_get_line({start, Port}) ->
receive
{Port, {data,Data}} ->
cgi_get_line([], {middle, Data, Port});
{Port, {exit_status, 0}} ->
?Debug("~p~n", [{exit_status, 0}]),
cgi_get_line([], {ending, <<>>, Port});
{Port, {exit_status, Status}} when Status /=0 ->
?Debug("~p~n", [{exit_status, Status}]),
{failure, {exit_status, Status}};
_Other ->
?Debug("~p~n", [_Other]),
cgi_get_line({start, Port})
end;
cgi_get_line(State) ->
cgi_get_line([], State).
cgi_get_line(Acc, {S, <<?ASCII_NEW_LINE, Tail/binary>>, Port}) ->
{lists:reverse(Acc), {S, Tail, Port}};
cgi_get_line(Acc, {S, <<?ASCII_CARRIAGE_RETURN, ?ASCII_NEW_LINE, Tail/binary>>,
Port}) ->
{lists:reverse(Acc), {S, Tail, Port}};
cgi_get_line(Acc, {middle, <<>>, Port}) ->
cgi_get_line(Acc, cgi_add_resp(<<>>, Port));
cgi_get_line(Acc, {middle, <<?ASCII_CARRIAGE_RETURN>>, Port}) ->
%% We SHOULD test for CRLF.
%% Would be easier without.
cgi_get_line(Acc, cgi_add_resp(<<?ASCII_CARRIAGE_RETURN>>, Port));
cgi_get_line(Acc, {ending, <<>>, Port}) ->
{lists:reverse(Acc), {ending, <<>>, Port}};
cgi_get_line(Acc, {S, <<C, Tail/binary>>, Port}) ->
cgi_get_line([C|Acc], {S, Tail, Port}).
cgi_add_resp(Bin, Port) ->
receive
{Port, {data,Data}} ->
{middle, <<Bin/binary, Data/binary>>, Port};
{Port, {exit_status, _Status}} ->
?Debug("~p~n", [{exit_status, _Status}]),
{ending, Bin, Port};
_Other ->
?Debug("~p~n", [_Other]),
cgi_add_resp(Bin, Port)
end.
%%%==============================================================================
%%% Code specific to FastCGI
%%%==============================================================================
-define(FCGI_VERSION_1, 1).
-define(FCGI_TYPE_BEGIN_REQUEST, 1).
%%% Not needed yet
%%%-define(FCGI_TYPE_ABORT_REQUEST, 2).
-define(FCGI_TYPE_END_REQUEST, 3).
-define(FCGI_TYPE_PARAMS, 4).
-define(FCGI_TYPE_STDIN, 5).
-define(FCGI_TYPE_STDOUT, 6).
-define(FCGI_TYPE_STDERR, 7).
%%% Not needed yet
%%%-define(FCGI_TYPE_DATA, 8).
%%%-define(FCGI_TYPE_GET_VALUES, 9).
%%%-define(FCGI_TYPE_GET_VALUES_RESULT, 10).
-define(FCGI_TYPE_UNKNOWN_TYPE, 11).
fcgi_type_name(?FCGI_TYPE_BEGIN_REQUEST) -> "begin-request";
%%% Not needed yet
%%%fcgi_type_name(?FCGI_TYPE_ABORT_REQUEST) -> "abort-request";
fcgi_type_name(?FCGI_TYPE_END_REQUEST) -> "end-request";
fcgi_type_name(?FCGI_TYPE_PARAMS) -> "params";
fcgi_type_name(?FCGI_TYPE_STDIN) -> "stdin";
fcgi_type_name(?FCGI_TYPE_STDOUT) -> "stdout";
fcgi_type_name(?FCGI_TYPE_STDERR) -> "stderr";
%%% Not needed yet
%%%fcgi_type_name(?FCGI_TYPE_DATA) -> "data";
%%%fcgi_type_name(?FCGI_TYPE_GET_VALUES) -> "get_values";
%%%fcgi_type_name(?FCGI_TYPE_GET_VALUES_RESULT) -> "get_values_result";
fcgi_type_name(?FCGI_TYPE_UNKNOWN_TYPE) -> "unknown-type".
%%% The FCGI implementation does not support handling concurrent requests
%%% over a connection; it creates a separate connection for each
%%% request. Hence, all application records have the same request-id,
%%% namely 1.
%%%
-define(FCGI_REQUEST_ID_MANAGEMENT, 0).
-define(FCGI_REQUEST_ID_APPLICATION, 1).
-define(FCGI_DONT_KEEP_CONN, 0).
-define(FCGI_KEEP_CONN, 1).
-define(FCGI_ROLE_RESPONDER, 1).
-define(FCGI_ROLE_AUTHORIZER, 2).
-define(FCGI_ROLE_FILTER, 3).
-ifdef(debug). % To avoid compile warning if debug is disabled.
fcgi_role_name(?FCGI_ROLE_RESPONDER) -> "responder";
fcgi_role_name(?FCGI_ROLE_AUTHORIZER) -> "authorizer";
fcgi_role_name(?FCGI_ROLE_FILTER) -> "filter";
fcgi_role_name(_) -> "?".
-endif.
-define(FCGI_STATUS_REQUEST_COMPLETE, 0).
-define(FCGI_STATUS_CANT_MPX_CONN, 1).
-define(FCGI_STATUS_OVERLOADED, 2).
-define(FCGI_STATUS_UNKNOWN_ROLE, 3).
fcgi_status_name(?FCGI_STATUS_REQUEST_COMPLETE) -> "request-complete";
fcgi_status_name(?FCGI_STATUS_CANT_MPX_CONN) -> "cannot-multiple-connection";
fcgi_status_name(?FCGI_STATUS_OVERLOADED) -> "overloaded";
fcgi_status_name(?FCGI_STATUS_UNKNOWN_ROLE) -> "unknown-role";
fcgi_status_name(_) -> "?".
%%% Amount of time (in milliseconds) allowed to connect to the application
%%% server.
%%%
-define(FCGI_CONNECT_TIMEOUT_MSECS, 10000).
%%% Amount of time (in milliseconds) allowed for data to arrive when
%%% reading the TCP connection to the application server.
%%%
-define(FCGI_READ_TIMEOUT_MSECS, 10000).
%%% TODO: Implement a configurable timeout which applies to the whole
%%% operation (as oposed to individual socket reads).
-record(fcgi_worker_state, {
app_server_host, % The hostname or IP address of
% the application server
app_server_port, % The TCP port number of the
% application server
path_info, % The path info
env, % All environment variables to be passed
% to the application (incl the extras)
keep_connection, % Delegate close authority to the
% application?
trace_protocol, % If true, log info messages for sent
% and received FastCGI messages
log_app_error, % If true, log error messages for
% application errors (stderr and
% non-zero exit)
role, % The role of the worker
% (responder, authorizer, filter)
parent_pid, % The PID of the parent process = the
% Yaws worker process
yaws_worker_pid, % When doing chunked output, stream to
% this Yaws worker.
app_server_socket, % The TCP socket to the FastCGI
% application server
stream_to_socket % The TCP socket to the web browser
% (stream chunked delivery to
% this socket)
}).
call_fcgi_responder(Arg) ->
call_fcgi_responder(Arg, []).
call_fcgi_responder(Arg, Options) ->
call_fcgi(?FCGI_ROLE_RESPONDER, Arg, Options).
call_fcgi_authorizer(Arg) ->
call_fcgi_authorizer(Arg, []).
call_fcgi_authorizer(Arg, Options) ->
Out = call_fcgi(?FCGI_ROLE_AUTHORIZER, Arg, Options),
case fcgi_is_access_allowed(Out) of
true ->
StrippedOut = strip_content_from_out(Out),
{allowed, StrippedOut};
false ->
{denied, Out}
end.
call_fcgi(Role, Arg, Options) ->
case Arg#arg.state of
{cgistate, WorkerPid} ->
case Arg#arg.cont of
cgicont ->
?Debug("Call FastCGI: continuation~n", []),
handle_clidata(Arg, WorkerPid)
end;
_ ->
?Debug("Call FastCGI:~n"
" Role = ~p (~s)~n"
" Options = ~p~n"
" Arg = ~p~n",
[Role, fcgi_role_name(Role),
Options,
Arg]),
ServerConf = get(sc),
WorkerPid = fcgi_start_worker(Role, Arg, ServerConf, Options),
handle_clidata(Arg, WorkerPid)
end.
is_not_content({content, _MimeType, _Content}) -> false;
is_not_content({streamcontent, _MimeType, _Content}) -> false;
is_not_content(_) -> true.
strip_content_from_out(Out) ->
lists:filter(fun is_not_content/1, Out).
fcgi_worker_fail(WorkerState, Reason) ->
ParentPid = WorkerState#fcgi_worker_state.parent_pid,
ParentPid ! {self(), failure, Reason},
error_logger:error_msg("FastCGI failure: ~p~n", [Reason]),
%% exit normally to avoid filling log with crash messages
exit(normal).
fcgi_worker_fail_if(true, WorkerState, Reason) ->
fcgi_worker_fail(WorkerState, Reason);
fcgi_worker_fail_if(_Condition, _WorkerState, _Reason) ->
ok.
fcgi_start_worker(Role, Arg, ServerConf, Options) ->
proc_lib:spawn(?MODULE, fcgi_worker,
[self(), Role, Arg, ServerConf, Options]).
fcgi_worker(ParentPid, Role, Arg, ServerConf, Options) ->
{DefaultSvrHost, DefaultSvrPort} =
case ServerConf#sconf.fcgi_app_server of
undefined ->
{undefined, undefined};
Else ->
Else
end,
AppServerHost = get_opt(app_server_host, Options, DefaultSvrHost),
AppServerPort = get_opt(app_server_port, Options, DefaultSvrPort),
PreliminaryWorkerState = #fcgi_worker_state{parent_pid = ParentPid},
fcgi_worker_fail_if(AppServerHost == undefined, PreliminaryWorkerState,
"app server host must be configured"),
fcgi_worker_fail_if(AppServerPort == undefined, PreliminaryWorkerState,
"app server port must be configured"),
PathInfo = get_opt(path_info, Options, Arg#arg.pathinfo),
ScriptFileName = Arg#arg.fullpath,
ExtraEnv = get_opt(extra_env, Options, []),
Env = build_env(Arg, ScriptFileName, PathInfo, ExtraEnv, ServerConf),
TraceProtocol = get_opt(trace_protocol, Options,
?sc_fcgi_trace_protocol(ServerConf)),
LogAppError = get_opt(log_app_error, Options,
?sc_fcgi_log_app_error(ServerConf)),
AppServerSocket =
fcgi_connect_to_application_server(PreliminaryWorkerState,
AppServerHost, AppServerPort),
?Debug("Start FastCGI worker:~n"
" Role = ~p (~s)~n"
" AppServerHost = ~p~n"
" AppServerPort = ~p~n"
" PathInfo = ~p~n"
" ExtraEnv = ~p~n"
" TraceProtocol = ~p~n"
" LogAppStderr = ~p~n",
[Role, fcgi_role_name(Role),
AppServerHost,
AppServerPort,
PathInfo,
ExtraEnv,
TraceProtocol,
LogAppError]),
WorkerState = #fcgi_worker_state{
app_server_host = AppServerHost,
app_server_port = AppServerPort,
path_info = PathInfo,
env = Env,
keep_connection = false, % Currently hard-coded; make
% configurable in the future?
trace_protocol = TraceProtocol,
log_app_error = LogAppError,
role = Role,
parent_pid = ParentPid,
yaws_worker_pid = Arg#arg.pid,
app_server_socket = AppServerSocket
},
fcgi_send_begin_request(WorkerState),
fcgi_send_params(WorkerState, Env),
fcgi_send_params(WorkerState, []),
fcgi_pass_through_client_data(WorkerState),
fcgi_header_loop(WorkerState),
gen_tcp:close(AppServerSocket),
ok.
fcgi_pass_through_client_data(WorkerState) ->
ParentPid = WorkerState#fcgi_worker_state.parent_pid,
receive
{ParentPid, clidata, ClientData} ->
ParentPid ! {self(), clidata_receipt},
fcgi_send_stdin(WorkerState, ClientData),
fcgi_pass_through_client_data(WorkerState);
{ParentPid, end_of_clidata} ->
fcgi_send_stdin(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
{error, Reason} ->
fcgi_worker_fail(WorkerState,
{"connect to application server failed", Reason});
{ok, Socket} ->
Socket
end.
fcgi_send_begin_request(WorkerState) ->
%% Not needed yet -- keep_connection is currently hard-coded to false
%%KeepConnection = WorkerState#fcgi_worker_state.keep_connection,
%%Flags = case KeepConnection of
%% true -> ?FCGI_KEEP_CONN;
%% false -> ?FCGI_DONT_KEEP_CONN
%% end,
Flags = ?FCGI_DONT_KEEP_CONN,
Role = WorkerState#fcgi_worker_state.role,
fcgi_send_record(WorkerState, ?FCGI_TYPE_BEGIN_REQUEST,
?FCGI_REQUEST_ID_APPLICATION, <<Role:16, Flags:8, 0:40>>).
fcgi_send_params(WorkerState, NameValueList) ->
fcgi_send_record(WorkerState, ?FCGI_TYPE_PARAMS,
?FCGI_REQUEST_ID_APPLICATION, NameValueList).
fcgi_send_stdin(WorkerState, Data) ->
fcgi_send_record(WorkerState, ?FCGI_TYPE_STDIN,
?FCGI_REQUEST_ID_APPLICATION, Data).
%%% Not needed yet
%%%
%%% fcgi_send_data(ParentPid, Socket, Data) ->
%%% fcgi_send_record(ParentPid, Socket, ?FCGI_TYPE_DATA,
%%% ?FCGI_REQUEST_ID_APPLICATION, Data).
%%% Not needed yet
%%%
%%% fcgi_send_abort_request(ParentPid, Socket) ->
%%% fcgi_send_record(ParentPid, Socket, ?FCGI_TYPE_ABORT_REQUEST,
%%% ?FCGI_REQUEST_ID_APPLICATION, <<>>).
fcgi_data_to_string(Data) ->
fcgi_data_to_string("", 0, "", "", Data).
fcgi_data_to_string(LinesStr, Count, CharStr, HexStr, <<>>) ->
if
Count == 0 ->
LinesStr;
true ->
Padding = lists:duplicate(16 - Count, $ ),
LinesStr ++ "\n " ++ CharStr ++ Padding ++ " " ++ HexStr
end;
fcgi_data_to_string(LinesStr, Count, CharStr, HexStr,
<<Byte:8, MoreData/binary>>) ->
Char = if
(Byte >= $!) and (Byte =< $~) ->
Byte;
true ->
$.
end,
Hex = io_lib:format("~2.16.0b ", [Byte]),
if
Count == 16 ->
fcgi_data_to_string(LinesStr ++ "\n " ++ CharStr ++ " " ++
HexStr, 1, [Char], Hex, MoreData);
true ->
fcgi_data_to_string(LinesStr, Count + 1, CharStr ++ [Char],
HexStr ++ Hex, MoreData)
end.
fcgi_trace_protocol(WorkerState, Action, Version, Type, RequestId,
ContentLength, PaddingLength, Reserved, ContentData,
PaddingData) ->
Trace = WorkerState#fcgi_worker_state.trace_protocol,
if
Trace ->
error_logger:info_msg(
"~s FastCGI record:~n"
" version = ~p~n"
" type = ~p (~s)~n"
" request-id = ~p~n"
" content-length = ~p~n"
" padding-length = ~p~n"
" reserved = ~p~n"
" content-data = ~s~n"
" padding-data = ~s~n",
[Action,
Version,
Type, fcgi_type_name(Type),
RequestId,
ContentLength,
PaddingLength,
Reserved,
fcgi_data_to_string(ContentData),
fcgi_data_to_string(PaddingData)]);
true ->
ok
end.
fcgi_send_record(WorkerState, Type, RequestId, NameValueList) ->
EncodedRecord = fcgi_encode_record(WorkerState, Type, RequestId,
NameValueList),
AppServerSocket = WorkerState#fcgi_worker_state.app_server_socket,
case gen_tcp:send(AppServerSocket, EncodedRecord) of
{error, Reason} ->
fcgi_worker_fail(WorkerState,
{"send to application server failed", Reason});
ok ->
ok
end.
fcgi_encode_record(WorkerState, Type, RequestId, NameValueList)
when is_list(NameValueList) ->
fcgi_encode_record(WorkerState, Type, RequestId,
fcgi_encode_name_value_list(NameValueList));
fcgi_encode_record(WorkerState, Type, RequestId, ContentData)
when is_binary(ContentData) ->
Version = 1,
ContentLength = size(ContentData),
%% Add padding bytes (if needed) to content bytes to make
%% content plus padding a multiple of 8 bytes.
PaddingLength = if
ContentLength rem 8 == 0 ->
0;
true ->
8 - (ContentLength rem 8)
end,
PaddingData = <<0:(PaddingLength * 8)>>,
Reserved = 0,
fcgi_trace_protocol(WorkerState, "Send", Version, Type, RequestId,
ContentLength, PaddingLength, Reserved,
ContentData, PaddingData),
<<Version:8,
Type:8,
RequestId:16,
ContentLength:16,
PaddingLength:8,
Reserved:8,
ContentData/binary,
PaddingData/binary>>.
fcgi_encode_name_value_list(_NameValueList = []) ->
<<>>;
fcgi_encode_name_value_list(_NameValueList = [{Name, Value} | Tail]) ->
<<(fcgi_encode_name_value(Name,Value))/binary,
(fcgi_encode_name_value_list(Tail))/binary>>.
fcgi_encode_name_value(Name, _Value = undefined) ->
fcgi_encode_name_value(Name, "");
fcgi_encode_name_value(Name, Value) when is_list(Name) and is_list(Value) ->
NameSize = iolist_size(Name),
%% If name size is < 128, encode it as one byte with the high bit clear.
%% If the name size >= 128, encoded it as 4 bytes with the high bit set.
NameSizeData = if
NameSize < 128 ->
<<NameSize:8>>;
true ->
<<(NameSize bor 16#80000000):32>>
end,
%% Same encoding for the value size.
ValueSize = iolist_size(Value),
ValueSizeData = if
ValueSize < 128 ->
<<ValueSize:8>>;
true ->
<<(ValueSize bor 16#80000000):32>>
end,
list_to_binary([<<NameSizeData/binary, ValueSizeData/binary>>, Name, Value]).
fcgi_header_loop(WorkerState) ->
fcgi_header_loop(WorkerState, start).
fcgi_header_loop(WorkerState, LineState) ->
Line = fcgi_get_line(WorkerState, LineState),
ParentPid = WorkerState#fcgi_worker_state.parent_pid,
case Line of
{failure, Reason} ->
ParentPid ! {self(), failure, Reason};
{_EmptyLine = [], NewLineState} ->
case NewLineState of
{middle, Data} ->
case WorkerState#fcgi_worker_state.role of
?FCGI_ROLE_AUTHORIZER ->
% For authorization we never stream to the client
fcgi_collect_all_data_loop(WorkerState, Data);
_ ->
ParentPid ! {self(), partial_data, Data},
receive
{ParentPid, stream_data} ->
fcgi_stream_data_loop(WorkerState);
{ParentPid, no_data} ->
ok
end
end;
{ending, Data} ->
ParentPid ! {self(), all_data, Data},
receive
{ParentPid, stream_data} ->
yaws_api:stream_chunk_end(
WorkerState#fcgi_worker_state.yaws_worker_pid);
{ParentPid, no_data} ->
ok
end
end;
{Header, NewLineState} ->
ParentPid ! {self(), header, Header},
fcgi_header_loop(WorkerState, NewLineState)
end.
fcgi_get_line(WorkerState, start) ->
case fcgi_get_output(WorkerState) of
{data, Data} ->
fcgi_get_line(WorkerState, [], {middle, Data});
{exit_status, 0} ->
fcgi_get_line(WorkerState, [], {ending, <<>>});
{exit_status, Status} when Status /=0 ->
{failure, {exit_status, Status}}
end;
fcgi_get_line(WorkerState, LineState) ->
fcgi_get_line(WorkerState, [], LineState).
fcgi_get_line(_WorkerState, Acc, {State, <<?ASCII_NEW_LINE, Tail/binary>>}) ->
{lists:reverse(Acc), {State, Tail}};
fcgi_get_line(_WorkerState, Acc, {State, <<?ASCII_CARRIAGE_RETURN,
?ASCII_NEW_LINE, Tail/binary>>}) ->
{lists:reverse(Acc), {State, Tail}};
fcgi_get_line(WorkerState, Acc, {middle, <<>>}) ->
fcgi_get_line(WorkerState, Acc, fcgi_add_resp(WorkerState, <<>>));
fcgi_get_line(WorkerState, Acc, {middle, <<?ASCII_CARRIAGE_RETURN>>}) ->
fcgi_get_line(WorkerState, Acc, fcgi_add_resp(WorkerState,
<<?ASCII_CARRIAGE_RETURN>>));
fcgi_get_line(_WorkerState, Acc, {ending, <<>>}) ->
{lists:reverse(Acc), {ending, <<>>}};
fcgi_get_line(WorkerState, Acc, {State, <<Char, Tail/binary>>}) ->
fcgi_get_line(WorkerState, [Char | Acc], {State, Tail}).
fcgi_add_resp(WorkerState, OldData) ->
case fcgi_get_output(WorkerState) of
{data, NewData} ->
{middle, <<OldData/binary, NewData/binary>>};
{exit_status, _Status} ->
{ending, OldData}
end.
fcgi_stream_data_loop(WorkerState) ->
YawsWorkerPid = WorkerState#fcgi_worker_state.yaws_worker_pid,
case fcgi_get_output(WorkerState) of
{data, Data} ->
yaws_api:stream_chunk_deliver_blocking(YawsWorkerPid, Data),
fcgi_stream_data_loop(WorkerState);
{exit_status, _Status} ->
yaws_api:stream_chunk_end(YawsWorkerPid)
end.
fcgi_collect_all_data_loop(WorkerState, Data) ->
YawsWorkerPid = WorkerState#fcgi_worker_state.yaws_worker_pid,
case fcgi_get_output(WorkerState) of
{data, MoreData} ->
NewData = <<Data/binary, MoreData/binary>>,
fcgi_collect_all_data_loop(WorkerState, NewData);
{exit_status, _Status} ->
ParentPid = WorkerState#fcgi_worker_state.parent_pid,
ParentPid ! {self(), all_data, Data},
receive
{ParentPid, stream_data} ->
yaws_api:stream_chunk_end(YawsWorkerPid);
{ParentPid, no_data} ->
ok
end
end.
fcgi_get_output(WorkerState) ->
{Type, ContentData} = fcgi_receive_record(WorkerState),
case Type of
?FCGI_TYPE_END_REQUEST ->
<<AppStatus:32/signed, ProtStatus:8, _Reserved:24>> = ContentData,
fcgi_worker_fail_if(ProtStatus < ?FCGI_STATUS_REQUEST_COMPLETE,
WorkerState,
{"received unknown protocol status",
ProtStatus}),
fcgi_worker_fail_if(ProtStatus > ?FCGI_STATUS_UNKNOWN_ROLE,
WorkerState,
{"received unknown protocol status",
ProtStatus}),
if
ProtStatus /= ?FCGI_STATUS_REQUEST_COMPLETE ->
error_logger:error_msg("FastCGI protocol error: ~p (~s)~n",
[ProtStatus,
fcgi_status_name(ProtStatus)]);
true ->
ok
end,
if
(AppStatus /= 0),
(WorkerState#fcgi_worker_state.log_app_error) ->
error_logger:error_msg(
"FastCGI application non-zero exit status: ~p~n",
[AppStatus]);
true ->
ok
end,
{exit_status, AppStatus};
?FCGI_TYPE_STDOUT ->
{data, ContentData};
?FCGI_TYPE_STDERR ->
if
(ContentData /= <<>>),
(WorkerState#fcgi_worker_state.log_app_error) ->
error_logger:error_msg(
"FastCGI application stderr output:~s~n",
[fcgi_data_to_string(ContentData)]);
true ->
ok
end,
fcgi_get_output(WorkerState);
?FCGI_TYPE_UNKNOWN_TYPE ->
<<UnknownType:8, _Reserved:56>> = ContentData,
fcgi_worker_fail(
WorkerState,
{"application did not understand record type we sent",
UnknownType})
end.
fcgi_receive_record(WorkerState) ->
Header = fcgi_receive_binary(WorkerState, 8, ?FCGI_READ_TIMEOUT_MSECS),
<<Version:8, Type:8, RequestId:16, ContentLength:16,
PaddingLength:8, Reserved:8>> = Header,
fcgi_worker_fail_if(Version /= 1, WorkerState,
{"received unsupported version", Version}),
case Type of
?FCGI_TYPE_END_REQUEST ->
fcgi_worker_fail_if(RequestId /= ?FCGI_REQUEST_ID_APPLICATION,
WorkerState,
{"unexpected request id", RequestId}),
fcgi_worker_fail_if(ContentLength /= 8, WorkerState,
{"incorrect content length for end request",
ContentLength}),
ok;
?FCGI_TYPE_STDOUT ->
fcgi_worker_fail_if(RequestId /= ?FCGI_REQUEST_ID_APPLICATION,
WorkerState,
{"unexpected request id", RequestId}),
ok;
?FCGI_TYPE_STDERR ->
fcgi_worker_fail_if(RequestId /= ?FCGI_REQUEST_ID_APPLICATION,
WorkerState,
{"unexpected request id", RequestId}),
ok;
?FCGI_TYPE_UNKNOWN_TYPE ->
fcgi_worker_fail_if(RequestId /= ?FCGI_REQUEST_ID_MANAGEMENT,
WorkerState,
{"unexpected request id", RequestId}),
fcgi_worker_fail_if(ContentLength /= 8, WorkerState,
{"incorrect content length for unknown type",
ContentLength}),
ok;
OtherType ->
throw({"received unexpected type", OtherType})
end,
ContentData = case ContentLength of
0 ->
<<>>;
_ ->
fcgi_receive_binary(WorkerState, ContentLength,
?FCGI_READ_TIMEOUT_MSECS)
end,
case PaddingLength of
0 ->
{Type, ContentData};
_ ->
PaddingData = fcgi_receive_binary(WorkerState, PaddingLength,
?FCGI_READ_TIMEOUT_MSECS),
fcgi_trace_protocol(WorkerState, "Receive",
Version, Type, RequestId,
ContentLength, PaddingLength,
Reserved, ContentData,
PaddingData),
{Type, ContentData}
end.
fcgi_receive_binary(_WorkerState, Length, _Timeout) when Length == 0 ->
<<>>;
fcgi_receive_binary(WorkerState, Length, Timeout) ->
AppServerSocket = WorkerState#fcgi_worker_state.app_server_socket,
case gen_tcp:recv(AppServerSocket, Length, Timeout) of
{error, Reason} ->
fcgi_worker_fail(WorkerState,
{"recv from application server failed", Reason});
{ok, Data} ->
Data
end.
%%% Access is allowed if, and only if, the resonse from the authorizer
%%% running on the application server contains a 200 OK status. Any other
%%% status or absence of a status means access is denied.
%%%
fcgi_is_access_allowed([Head | Tail]) ->
fcgi_is_access_allowed(Head) orelse fcgi_is_access_allowed(Tail);
fcgi_is_access_allowed({status, 200}) ->
true;
fcgi_is_access_allowed(_AnythingElse) ->
false.
%%% Look for headers of the form "Variable-VAR_NAME: var value"
%%%
fcgi_extract_variables([Head | Tail]) ->
fcgi_extract_variables(Head) ++ fcgi_extract_variables(Tail);
fcgi_extract_variables({header, "Variable-" ++ Rest}) ->
[fcgi_split_header(Rest)];
fcgi_extract_variables(_AnythingElse) ->
[].
fcgi_split_header(Header) ->
fcgi_split_header(name, [], [], Header).
fcgi_split_header(_, NameAcc, ValueAcc, "") ->
{string:strip(lists:reverse(NameAcc)),
string:strip(lists:reverse(ValueAcc))};
fcgi_split_header(name, NameAcc, ValueAcc, [$: | MoreStr]) ->
fcgi_split_header(value, NameAcc, ValueAcc, MoreStr);
fcgi_split_header(name, NameAcc, ValueAcc, [Char | MoreStr]) ->
fcgi_split_header(name, [Char | NameAcc], ValueAcc, MoreStr);
fcgi_split_header(value, NameAcc, ValueAcc, [Char | MoreStr]) ->
fcgi_split_header(value, NameAcc, [Char | ValueAcc], MoreStr).
Jump to Line
Something went wrong with that request. Please try again.