%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% %% Implements The WWW Common Gateway Interface Version 1.1 -module(mod_cgi). -export([env/3]). %%% Callback API -export([do/1, load/2, store/2]). -include("http_internal.hrl"). -include("httpd_internal.hrl"). -include("httpd.hrl"). -define(VMODULE,"CGI"). -define(DEFAULT_CGI_TIMEOUT, 15000). %%%========================================================================= %%% API %%%========================================================================= %%-------------------------------------------------------------------------- %% do(ModData, _, AfterScript) -> [{EnvVariable, Value}] %% %% AfterScript = string() %% ModData = #mod{} %% EnvVariable = string() %% Value = term() %% Description: Keep for now as it is documented in the man page %%------------------------------------------------------------------------- env(ModData, _Script, AfterScript) -> ScriptElements = script_elements(ModData, AfterScript), httpd_script_env:create_env(cgi, ModData, ScriptElements). %%%========================================================================= %%% Callback API %%%========================================================================= %%-------------------------------------------------------------------------- %% do(ModData) -> {proceed, OldData} | {proceed, NewData} | {break, NewData} %% | done %% ModData = #mod{} %% %% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS %%------------------------------------------------------------------------- do(ModData) -> case proplists:get_value(status, ModData#mod.data) of %% A status code has been generated! {_StatusCode, _PhraseArgs, _Reason} -> {proceed, ModData#mod.data}; %% No status code has been generated! undefined -> case proplists:get_value(response, ModData#mod.data) of undefined -> generate_response(ModData); _Response -> {proceed, ModData#mod.data} end end. %%-------------------------------------------------------------------------- %% load(Line, Context) -> eof | ok | {ok, NewContext} | %% {ok, NewContext, Directive} | %% {ok, NewContext, DirectiveList} | {error, Reason} %% Line = string() %% Context = NewContext = DirectiveList = [Directive] %% Directive = {DirectiveKey , DirectiveValue} %% DirectiveKey = DirectiveValue = term() %% Reason = term() %% %% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS %%------------------------------------------------------------------------- %% ScriptNoCache true|false, defines whether the server shall add %% header fields to stop proxies and %% clients from saving the page in history %% or cache %% load("ScriptNoCache " ++ CacheArg, [])-> case catch list_to_atom(string:strip(CacheArg)) of true -> {ok, [], {script_nocache, true}}; false -> {ok, [], {script_nocache, false}}; _ -> {error, ?NICE(string:strip(CacheArg)++ " is an invalid ScriptNoCache directive")} end; %% ScriptTimeout Seconds, The number of seconds that the server %% maximum will wait for the script to %% generate a part of the document load("ScriptTimeout " ++ Timeout, [])-> case catch list_to_integer(string:strip(Timeout)) of TimeoutSec when is_integer(TimeoutSec) -> {ok, [], {script_timeout,TimeoutSec*1000}}; _ -> {error, ?NICE(string:strip(Timeout)++ " is an invalid ScriptTimeout")} end. %%-------------------------------------------------------------------------- %% store(Directive, DirectiveList) -> {ok, NewDirective} | %% {ok, [NewDirective]} | %% {error, Reason} %% Directive = {DirectiveKey , DirectiveValue} %% DirectiveKey = DirectiveValue = term() %% Reason = term() %% %% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS %%------------------------------------------------------------------------- store({script_nocache, Value} = Conf, _) when Value == true; Value == false -> {ok, Conf}; store({script_nocache, Value}, _) -> {error, {wrong_type, {script_nocache, Value}}}; store({script_timeout, Value}, _) when is_integer(Value), Value >= 0 -> {ok, {script_timeout, Value * 1000}}; store({script_timeout, Value}, _) -> {error, {wrong_type, {script_timeout, Value}}}. %%%======================================================================== %%% Internal functions %%%======================================================================== generate_response(ModData) -> RequestURI = case proplists:get_value(new_request_uri, ModData#mod.data) of undefined -> ModData#mod.request_uri; Value -> Value end, ScriptAliases = httpd_util:multi_lookup(ModData#mod.config_db, script_alias), case mod_alias:real_script_name(ModData#mod.config_db, RequestURI, ScriptAliases) of {Script, AfterScript} -> exec_script(ModData, Script, AfterScript, RequestURI); not_a_script -> {proceed, ModData#mod.data} end. is_executable(File) -> Dir = filename:dirname(File), FileName = filename:basename(File), case os:type() of {win32,_} -> %% temporary (hopefully) fix for win32 OTP-3627 is_win32_executable(Dir,FileName); _ -> is_executable(Dir, FileName) end. is_executable(Dir, FilName) -> case os:find_executable(FilName, Dir) of false -> false; _ -> true end. %% Start temporary (hopefully) fix for win32 OTP-3627 %% --------------------------------- is_win32_executable(Dir, FileName) -> NewFileName = strip_extention(FileName, [".bat",".exe",".com", ".cmd"]), is_executable(Dir, NewFileName). strip_extention(FileName, []) -> FileName; strip_extention(FileName, [Extention | Extentions]) -> case filename:basename(FileName, Extention) of FileName -> strip_extention(FileName, Extentions); NewFileName -> NewFileName end. %% End fix %% --------------------------------- exec_script(ModData, Script, AfterScript, RequestURI) -> exec_script(is_executable(Script), ModData, Script, AfterScript, RequestURI). exec_script(true, ModData, Script, AfterScript, _RequestURI) -> process_flag(trap_exit,true), Dir = filename:dirname(Script), ScriptElements = script_elements(ModData, AfterScript), Env = (catch httpd_script_env:create_env(cgi, ModData, ScriptElements)), %% Run script Port = (catch open_port({spawn, Script},[binary, stream, {cd, Dir}, {env, Env}])), case Port of Port when is_port(Port) -> send_request_body_to_script(ModData, Port), deliver_webpage(ModData, Port); % Take care of script output Error -> exit({open_port_failed, Error, [{mod,?MODULE}, {uri,ModData#mod.request_uri}, {script,Script}, {env,Env},{dir,Dir}]}) end; exec_script(false, ModData, _Script, _AfterScript, _RequestURI) -> {proceed, [{status, {404,ModData#mod.request_uri, ?NICE("You don't have permission to execute " ++ ModData#mod.request_uri ++ " on this server")}}| ModData#mod.data]}. send_request_body_to_script(ModData, Port) -> case ModData#mod.entity_body of [] -> ok; EntityBody -> port_command(Port, EntityBody) end. deliver_webpage(#mod{config_db = Db} = ModData, Port) -> Timeout = script_timeout(Db), case receive_headers(Port, httpd_cgi, parse_headers, [<<>>, [], []], Timeout) of {Headers, Body} -> case httpd_cgi:handle_headers(Headers) of {proceed, AbsPath} -> {proceed, [{real_name, httpd_util:split_path(AbsPath)} | ModData#mod.data]}; {ok, HTTPHeaders, Status} -> IsDisableChunkedSend = httpd_response:is_disable_chunked_send(Db), case (ModData#mod.http_version =/= "HTTP/1.1") or (IsDisableChunkedSend) of true -> send_headers(ModData, Status, [{"connection", "close"} | HTTPHeaders]); false -> send_headers(ModData, Status, [{"transfer-encoding", "chunked"} | HTTPHeaders]) end, handle_body(Port, ModData, Body, Timeout, size(Body), IsDisableChunkedSend) end; {'EXIT', Port, Reason} -> process_flag(trap_exit, false), {proceed, [{status, {400, none, reason(Reason)}} | ModData#mod.data]}; timeout -> (catch port_close(Port)), % KILL the port !!!! send_headers(ModData, {504, "Timeout"}, []), httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket), process_flag(trap_exit,false), {proceed,[{response, {already_sent, 200, 0}} | ModData#mod.data]} end. receive_headers(Port, Module, Function, Args, Timeout) -> receive {Port, {data, Response}} when is_port(Port) -> case Module:Function([Response | Args]) of {NewModule, NewFunction, NewArgs} -> receive_headers(Port, NewModule, NewFunction, NewArgs, Timeout); {ok, {Headers, Body}} -> {Headers, Body} end; {'EXIT', Port, Reason} when is_port(Port) -> {'EXIT', Port, Reason}; {'EXIT', Pid, Reason} when is_pid(Pid) -> exit({linked_process_died, Pid, Reason}) after Timeout -> timeout end. send_headers(ModData, {StatusCode, _}, HTTPHeaders) -> ExtraHeaders = httpd_response:cache_headers(ModData, script_nocache), httpd_response:send_header(ModData, StatusCode, ExtraHeaders ++ HTTPHeaders). handle_body(Port, #mod{method = "HEAD"} = ModData, _, _, Size, _) -> (catch port_close(Port)), % KILL the port !!!! process_flag(trap_exit,false), {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; handle_body(Port, ModData, Body, Timeout, Size, IsDisableChunkedSend) -> httpd_response:send_chunk(ModData, Body, IsDisableChunkedSend), receive {Port, {data, Data}} when is_port(Port) -> handle_body(Port, ModData, Data, Timeout, Size + size(Data), IsDisableChunkedSend); {'EXIT', Port, normal} when is_port(Port) -> httpd_response:send_final_chunk(ModData, IsDisableChunkedSend), process_flag(trap_exit,false), {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; {'EXIT', Port, Reason} when is_port(Port) -> process_flag(trap_exit, false), {proceed, [{status, {400, none, reason(Reason)}} | ModData#mod.data]}; {'EXIT', Pid, Reason} when is_pid(Pid) -> exit({mod_cgi_linked_process_died, Pid, Reason}) after Timeout -> (catch port_close(Port)), % KILL the port !!!! process_flag(trap_exit,false), {proceed,[{response, {already_sent, 200, Size}} | ModData#mod.data]} end. script_elements(#mod{method = "GET"}, {[], QueryString}) -> [{query_string, QueryString}]; script_elements(#mod{method = "GET"}, {PathInfo, []}) -> [{path_info, PathInfo}]; script_elements(#mod{method = "GET"}, {PathInfo, QueryString}) -> [{query_string, QueryString}, {path_info, PathInfo}]; script_elements(#mod{method = "POST", entity_body = Body}, _) -> [{entity_body, Body}]; script_elements(#mod{method = "PATCH", entity_body = Body}, _) -> [{entity_body, Body}]; script_elements(#mod{method = "PUT", entity_body = Body}, _) -> [{entity_body, Body}]; script_elements(_, _) -> []. script_timeout(Db) -> httpd_util:lookup(Db, script_timeout, ?DEFAULT_CGI_TIMEOUT). %% Convert error to printable string %% reason({error,emfile}) -> ": To many open files"; reason({error,{enfile,_}}) -> ": File/port table overflow"; reason({error,enomem}) -> ": Not enough memory"; reason({error,eagain}) -> ": No more available OS processes"; reason(Reason) -> lists:flatten(io_lib:format("Reason: ~p~n", [Reason])).