1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 1997-2021. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20%% 21%% Implements The WWW Common Gateway Interface Version 1.1 22 23-module(mod_cgi). 24 25-export([env/3]). 26 27%%% Callback API 28-export([do/1, store/2]). 29 30-include("http_internal.hrl"). 31-include("httpd_internal.hrl"). 32-include("httpd.hrl"). 33 34-define(VMODULE,"CGI"). 35 36-define(DEFAULT_CGI_TIMEOUT, 15000). 37 38%%%========================================================================= 39%%% API 40%%%========================================================================= 41%%-------------------------------------------------------------------------- 42%% do(ModData, _, AfterScript) -> [{EnvVariable, Value}] 43%% 44%% AfterScript = string() 45%% ModData = #mod{} 46%% EnvVariable = string() 47%% Value = term() 48%% Description: Keep for now as it is documented in the man page 49%%------------------------------------------------------------------------- 50env(ModData, _Script, AfterScript) -> 51 ScriptElements = script_elements(ModData, AfterScript), 52 httpd_script_env:create_env(cgi, ModData, ScriptElements). 53 54%%%========================================================================= 55%%% Callback API 56%%%========================================================================= 57 58%%-------------------------------------------------------------------------- 59%% do(ModData) -> {proceed, OldData} | {proceed, NewData} | {break, NewData} 60%% | done 61%% ModData = #mod{} 62%% 63%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS 64%%------------------------------------------------------------------------- 65do(ModData) -> 66 case proplists:get_value(status, ModData#mod.data) of 67 %% A status code has been generated! 68 {_StatusCode, _PhraseArgs, _Reason} -> 69 {proceed, ModData#mod.data}; 70 %% No status code has been generated! 71 undefined -> 72 case proplists:get_value(response, ModData#mod.data) of 73 undefined -> 74 generate_response(ModData); 75 _Response -> 76 {proceed, ModData#mod.data} 77 end 78 end. 79 80%%-------------------------------------------------------------------------- 81%% store(Directive, DirectiveList) -> {ok, NewDirective} | 82%% {ok, [NewDirective]} | 83%% {error, Reason} 84%% Directive = {DirectiveKey , DirectiveValue} 85%% DirectiveKey = DirectiveValue = term() 86%% Reason = term() 87%% 88%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS 89%%------------------------------------------------------------------------- 90store({script_nocache, Value} = Conf, _) 91 when Value == true; Value == false -> 92 {ok, Conf}; 93store({script_nocache, Value}, _) -> 94 {error, {wrong_type, {script_nocache, Value}}}; 95store({script_timeout, Value}, _) 96 when is_integer(Value), Value >= 0 -> 97 {ok, {script_timeout, Value * 1000}}; 98store({script_timeout, Value}, _) -> 99 {error, {wrong_type, {script_timeout, Value}}}. 100 101%%%======================================================================== 102%%% Internal functions 103%%%======================================================================== 104generate_response(ModData) -> 105 RequestURI = 106 case proplists:get_value(new_request_uri, ModData#mod.data) of 107 undefined -> 108 ModData#mod.request_uri; 109 Value -> 110 Value 111 end, 112 ScriptAliases = 113 httpd_util:multi_lookup(ModData#mod.config_db, script_alias), 114 case mod_alias:real_script_name(ModData#mod.config_db, RequestURI, 115 ScriptAliases) of 116 {Script, AfterScript} -> 117 exec_script(ModData, Script, AfterScript, 118 RequestURI); 119 not_a_script -> 120 {proceed, ModData#mod.data} 121 end. 122 123is_executable(File) -> 124 Dir = filename:dirname(File), 125 FileName = filename:basename(File), 126 case os:type() of 127 {win32,_} -> 128 %% temporary (hopefully) fix for win32 OTP-3627 129 is_win32_executable(Dir,FileName); 130 _ -> 131 is_executable(Dir, FileName) 132 end. 133 134is_executable(Dir, FilName) -> 135 case os:find_executable(FilName, Dir) of 136 false -> 137 false; 138 _ -> 139 true 140 end. 141 142%% Start temporary (hopefully) fix for win32 OTP-3627 143%% --------------------------------- 144is_win32_executable(Dir, FileName) -> 145 NewFileName = strip_extention(FileName, [".bat",".exe",".com", ".cmd"]), 146 is_executable(Dir, NewFileName). 147 148strip_extention(FileName, []) -> 149 FileName; 150strip_extention(FileName, [Extention | Extentions]) -> 151 case filename:basename(FileName, Extention) of 152 FileName -> 153 strip_extention(FileName, Extentions); 154 NewFileName -> 155 NewFileName 156 end. 157 158%% End fix 159%% --------------------------------- 160 161exec_script(ModData, Script, AfterScript, RequestURI) -> 162 exec_script(is_executable(Script), ModData, Script, 163 AfterScript, RequestURI). 164 165exec_script(true, ModData, Script, AfterScript, _RequestURI) -> 166 process_flag(trap_exit,true), 167 Dir = filename:dirname(Script), 168 ScriptElements = script_elements(ModData, AfterScript), 169 Env = (catch httpd_script_env:create_env(cgi, ModData, ScriptElements)), 170 171 %% Run script 172 Port = (catch open_port({spawn, Script},[binary, stream, 173 {cd, Dir}, {env, Env}])), 174 case Port of 175 Port when is_port(Port) -> 176 send_request_body_to_script(ModData, Port), 177 deliver_webpage(ModData, Port); % Take care of script output 178 Error -> 179 exit({open_port_failed, Error, 180 [{mod,?MODULE}, 181 {uri,ModData#mod.request_uri}, {script,Script}, 182 {env,Env},{dir,Dir}]}) 183 end; 184 185exec_script(false, ModData, _Script, _AfterScript, _RequestURI) -> 186 {proceed, 187 [{status, 188 {404,ModData#mod.request_uri, 189 ?NICE("You don't have permission to execute " ++ 190 ModData#mod.request_uri ++ " on this server")}}| 191 ModData#mod.data]}. 192 193send_request_body_to_script(ModData, Port) -> 194 case ModData#mod.entity_body of 195 [] -> 196 ok; 197 EntityBody -> 198 port_command(Port, EntityBody) 199 end. 200 201deliver_webpage(#mod{config_db = Db} = ModData, Port) -> 202 Timeout = script_timeout(Db), 203 case receive_headers(Port, httpd_cgi, parse_headers, 204 [<<>>, [], []], Timeout) of 205 {Headers, Body} -> 206 case httpd_cgi:handle_headers(Headers) of 207 {proceed, AbsPath} -> 208 {proceed, [{real_name, 209 httpd_util:split_path(AbsPath)} | 210 ModData#mod.data]}; 211 {ok, HTTPHeaders, Status} -> 212 IsDisableChunkedSend = 213 httpd_response:is_disable_chunked_send(Db), 214 case (ModData#mod.http_version =/= "HTTP/1.1") or 215 (IsDisableChunkedSend) of 216 true -> 217 send_headers(ModData, Status, 218 [{"connection", "close"} 219 | HTTPHeaders]); 220 false -> 221 send_headers(ModData, Status, 222 [{"transfer-encoding", 223 "chunked"} | HTTPHeaders]) 224 end, 225 handle_body(Port, ModData, Body, Timeout, size(Body), 226 IsDisableChunkedSend) 227 end; 228 {'EXIT', Port, Reason} -> 229 process_flag(trap_exit, false), 230 {proceed, [{status, {400, none, reason(Reason)}} | 231 ModData#mod.data]}; 232 timeout -> 233 (catch port_close(Port)), % KILL the port !!!! 234 send_headers(ModData, {504, "Timeout"}, []), 235 httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket), 236 process_flag(trap_exit,false), 237 {proceed,[{response, {already_sent, 200, 0}} | ModData#mod.data]} 238 end. 239 240receive_headers(Port, Module, Function, Args, Timeout) -> 241 receive 242 {Port, {data, Response}} when is_port(Port) -> 243 case Module:Function([Response | Args]) of 244 {NewModule, NewFunction, NewArgs} -> 245 receive_headers(Port, NewModule, 246 NewFunction, NewArgs, Timeout); 247 {ok, {Headers, Body}} -> 248 {Headers, Body} 249 end; 250 {'EXIT', Port, Reason} when is_port(Port) -> 251 {'EXIT', Port, Reason}; 252 {'EXIT', Pid, Reason} when is_pid(Pid) -> 253 exit({linked_process_died, Pid, Reason}) 254 after Timeout -> 255 timeout 256 end. 257 258send_headers(ModData, {StatusCode, _}, HTTPHeaders) -> 259 ExtraHeaders = httpd_response:cache_headers(ModData, script_nocache), 260 httpd_response:send_header(ModData, StatusCode, 261 ExtraHeaders ++ HTTPHeaders). 262 263handle_body(Port, #mod{method = "HEAD"} = ModData, _, _, Size, _) -> 264 (catch port_close(Port)), % KILL the port !!!! 265 process_flag(trap_exit,false), 266 {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; 267 268handle_body(Port, ModData, Body, Timeout, Size, IsDisableChunkedSend) -> 269 httpd_response:send_chunk(ModData, Body, IsDisableChunkedSend), 270 receive 271 {Port, {data, Data}} when is_port(Port) -> 272 handle_body(Port, ModData, Data, Timeout, Size + size(Data), 273 IsDisableChunkedSend); 274 {'EXIT', Port, normal} when is_port(Port) -> 275 httpd_response:send_final_chunk(ModData, IsDisableChunkedSend), 276 process_flag(trap_exit,false), 277 {proceed, [{response, {already_sent, 200, Size}} | 278 ModData#mod.data]}; 279 {'EXIT', Port, Reason} when is_port(Port) -> 280 process_flag(trap_exit, false), 281 {proceed, [{status, {400, none, reason(Reason)}} | 282 ModData#mod.data]}; 283 {'EXIT', Pid, Reason} when is_pid(Pid) -> 284 exit({mod_cgi_linked_process_died, Pid, Reason}) 285 after Timeout -> 286 (catch port_close(Port)), % KILL the port !!!! 287 process_flag(trap_exit,false), 288 {proceed,[{response, {already_sent, 200, Size}} | 289 ModData#mod.data]} 290 end. 291 292script_elements(#mod{method = "GET"}, {[], QueryString}) -> 293 [{query_string, QueryString}]; 294script_elements(#mod{method = "GET"}, {PathInfo, []}) -> 295 [{path_info, PathInfo}]; 296script_elements(#mod{method = "GET"}, {PathInfo, QueryString}) -> 297 [{query_string, QueryString}, {path_info, PathInfo}]; 298script_elements(#mod{method = "POST", entity_body = Body}, _) -> 299 [{entity_body, Body}]; 300script_elements(#mod{method = "PATCH", entity_body = Body}, _) -> 301 [{entity_body, Body}]; 302script_elements(#mod{method = "PUT", entity_body = Body}, _) -> 303 [{entity_body, Body}]; 304script_elements(_, _) -> 305 []. 306 307script_timeout(Db) -> 308 httpd_util:lookup(Db, script_timeout, ?DEFAULT_CGI_TIMEOUT). 309 310%% Convert error to printable string 311%% 312reason({error,emfile}) -> ": To many open files"; 313reason({error,{enfile,_}}) -> ": File/port table overflow"; 314reason({error,enomem}) -> ": Not enough memory"; 315reason({error,eagain}) -> ": No more available OS processes"; 316reason(Reason) -> lists:flatten(io_lib:format("Reason: ~p~n", [Reason])). 317