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