View Javadoc

1   //========================================================================
2   //Copyright 2006 Mort Bay Consulting Pty. Ltd.
3   //------------------------------------------------------------------------
4   //Licensed under the Apache License, Version 2.0 (the "License");
5   //you may not use this file except in compliance with the License.
6   //You may obtain a copy of the License at 
7   //http://www.apache.org/licenses/LICENSE-2.0
8   //Unless required by applicable law or agreed to in writing, software
9   //distributed under the License is distributed on an "AS IS" BASIS,
10  //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  //See the License for the specific language governing permissions and
12  //limitations under the License.
13  //========================================================================
14  
15  package org.mortbay.servlet;
16  
17  import java.io.BufferedReader;
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.InputStreamReader;
22  import java.io.OutputStream;
23  import java.io.PrintWriter;
24  import java.util.Enumeration;
25  import java.util.HashMap;
26  import java.util.Map;
27  
28  import javax.servlet.ServletException;
29  import javax.servlet.http.HttpServlet;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletResponse;
32  
33  import org.mortbay.log.Log;
34  import org.mortbay.util.IO;
35  import org.mortbay.util.StringUtil;
36  
37  //-----------------------------------------------------------------------------
38  /**
39   * CGI Servlet.
40   * 
41   * The cgi bin directory can be set with the "cgibinResourceBase" init parameter
42   * or it will default to the resource base of the context.
43   * 
44   * The "commandPrefix" init parameter may be used to set a prefix to all
45   * commands passed to exec. This can be used on systems that need assistance to
46   * execute a particular file type. For example on windows this can be set to
47   * "perl" so that perl scripts are executed.
48   * 
49   * The "Path" init param is passed to the exec environment as PATH. Note: Must
50   * be run unpacked somewhere in the filesystem.
51   * 
52   * Any initParameter that starts with ENV_ is used to set an environment
53   * variable with the name stripped of the leading ENV_ and using the init
54   * parameter value.
55   * 
56   * @author Julian Gosnell
57   * @author Thanassis Papathanasiou - Some minor modifications for Jetty6 port
58   */
59  public class CGI extends HttpServlet
60  {
61      private boolean _ok;
62      private File _docRoot;
63      private String _path;
64      private String _cmdPrefix;
65      private EnvList _env;
66      private boolean _ignoreExitState;
67  
68      /* ------------------------------------------------------------ */
69      public void init() throws ServletException
70      {
71          _env=new EnvList();
72          _cmdPrefix=getInitParameter("commandPrefix");
73  
74          String tmp=getInitParameter("cgibinResourceBase");
75          if (tmp==null)
76          {
77              tmp=getInitParameter("resourceBase");
78              if (tmp==null)
79                  tmp=getServletContext().getRealPath("/");
80          }
81  
82          if (tmp==null)
83          {
84              Log.warn("CGI: no CGI bin !");
85              return;
86          }
87  
88          File dir=new File(tmp);
89          if (!dir.exists())
90          {
91              Log.warn("CGI: CGI bin does not exist - "+dir);
92              return;
93          }
94  
95          if (!dir.canRead())
96          {
97              Log.warn("CGI: CGI bin is not readable - "+dir);
98              return;
99          }
100 
101         if (!dir.isDirectory())
102         {
103             Log.warn("CGI: CGI bin is not a directory - "+dir);
104             return;
105         }
106 
107         try
108         {
109             _docRoot=dir.getCanonicalFile();
110         }
111         catch (IOException e)
112         {
113             Log.warn("CGI: CGI bin failed - "+dir,e);
114             return;
115         }
116 
117         _path=getInitParameter("Path");
118         if (_path!=null)
119             _env.set("PATH",_path);
120 
121         _ignoreExitState="true".equalsIgnoreCase(getInitParameter("ignoreExitState"));
122         Enumeration e=getInitParameterNames();
123         while (e.hasMoreElements())
124         {
125             String n=(String)e.nextElement();
126             if (n!=null&&n.startsWith("ENV_"))
127                 _env.set(n.substring(4),getInitParameter(n));
128         }
129         if(!_env.envMap.containsKey("SystemRoot"))
130         {
131       	    String os = System.getProperty("os.name");
132             if (os!=null && os.toLowerCase().indexOf("windows")!=-1)
133             {
134         	String windir = System.getProperty("windir");
135         	_env.set("SystemRoot", windir!=null ? windir : "C:\\WINDOWS"); 
136             }
137         }   
138       
139         _ok=true;
140     }
141 
142     /* ------------------------------------------------------------ */
143     public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
144     {
145         if (!_ok)
146         {
147             res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
148             return;
149         }
150         
151         String pathInContext=StringUtil.nonNull(req.getServletPath())+StringUtil.nonNull(req.getPathInfo());
152 
153         if (Log.isDebugEnabled())
154         {
155             Log.debug("CGI: ContextPath : "+req.getContextPath());
156             Log.debug("CGI: ServletPath : "+req.getServletPath());
157             Log.debug("CGI: PathInfo    : "+req.getPathInfo());
158             Log.debug("CGI: _docRoot    : "+_docRoot);
159             Log.debug("CGI: _path       : "+_path);
160             Log.debug("CGI: _ignoreExitState: "+_ignoreExitState);
161         }
162 
163         // pathInContext may actually comprises scriptName/pathInfo...We will
164         // walk backwards up it until we find the script - the rest must
165         // be the pathInfo;
166 
167         String both=pathInContext;
168         String first=both;
169         String last="";
170 
171         File exe=new File(_docRoot,first);
172 
173         while ((first.endsWith("/")||!exe.exists())&&first.length()>=0)
174         {
175             int index=first.lastIndexOf('/');
176 
177             first=first.substring(0,index);
178             last=both.substring(index,both.length());
179             exe=new File(_docRoot,first);
180         }
181 
182         if (first.length()==0||!exe.exists()||exe.isDirectory()||!exe.getCanonicalPath().equals(exe.getAbsolutePath()))
183         {
184             res.sendError(404);
185         }
186         else
187         {
188             if (Log.isDebugEnabled())
189             {
190                 Log.debug("CGI: script is "+exe);
191                 Log.debug("CGI: pathInfo is "+last);
192             }
193             exec(exe,last,req,res);
194         }
195     }
196 
197     /* ------------------------------------------------------------ */
198     /*
199      * @param root @param path @param req @param res @exception IOException
200      */
201     private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException
202     {
203         String path=command.getAbsolutePath();
204         File dir=command.getParentFile();
205         String scriptName=req.getRequestURI().substring(0,req.getRequestURI().length()-pathInfo.length());
206         String scriptPath=getServletContext().getRealPath(scriptName);
207         String pathTranslated=req.getPathTranslated();
208 
209         int len=req.getContentLength();
210         if (len<0)
211             len=0;
212         if ((pathTranslated==null)||(pathTranslated.length()==0))
213             pathTranslated=path;
214 
215         EnvList env=new EnvList(_env);
216         // these ones are from "The WWW Common Gateway Interface Version 1.1"
217         // look at :
218         // http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
219         env.set("AUTH_TYPE",req.getAuthType());
220         env.set("CONTENT_LENGTH",Integer.toString(len));
221         env.set("CONTENT_TYPE",req.getContentType());
222         env.set("GATEWAY_INTERFACE","CGI/1.1");
223         if ((pathInfo!=null)&&(pathInfo.length()>0))
224         {
225             env.set("PATH_INFO",pathInfo);
226         }
227         env.set("PATH_TRANSLATED",pathTranslated);
228         env.set("QUERY_STRING",req.getQueryString());
229         env.set("REMOTE_ADDR",req.getRemoteAddr());
230         env.set("REMOTE_HOST",req.getRemoteHost());
231         // The identity information reported about the connection by a
232         // RFC 1413 [11] request to the remote agent, if
233         // available. Servers MAY choose not to support this feature, or
234         // not to request the data for efficiency reasons.
235         // "REMOTE_IDENT" => "NYI"
236         env.set("REMOTE_USER",req.getRemoteUser());
237         env.set("REQUEST_METHOD",req.getMethod());
238         env.set("SCRIPT_NAME",scriptName);
239         env.set("SCRIPT_FILENAME",scriptPath);
240         env.set("SERVER_NAME",req.getServerName());
241         env.set("SERVER_PORT",Integer.toString(req.getServerPort()));
242         env.set("SERVER_PROTOCOL",req.getProtocol());
243         env.set("SERVER_SOFTWARE",getServletContext().getServerInfo());
244 
245         Enumeration enm=req.getHeaderNames();
246         while (enm.hasMoreElements())
247         {
248             String name=(String)enm.nextElement();
249             String value=req.getHeader(name);
250             env.set("HTTP_"+name.toUpperCase().replace('-','_'),value);
251         }
252 
253         // these extra ones were from printenv on www.dev.nomura.co.uk
254         env.set("HTTPS",(req.isSecure()?"ON":"OFF"));
255         // "DOCUMENT_ROOT" => root + "/docs",
256         // "SERVER_URL" => "NYI - http://us0245",
257         // "TZ" => System.getProperty("user.timezone"),
258 
259         // are we meant to decode args here ? or does the script get them
260         // via PATH_INFO ? if we are, they should be decoded and passed
261         // into exec here...
262         String execCmd=path;
263         if ((execCmd.charAt(0)!='"')&&(execCmd.indexOf(" ")>=0))
264             execCmd="\""+execCmd+"\"";
265         if (_cmdPrefix!=null)
266             execCmd=_cmdPrefix+" "+execCmd;
267 
268         Process p=(dir==null)?Runtime.getRuntime().exec(execCmd,env.getEnvArray()):Runtime.getRuntime().exec(execCmd,env.getEnvArray(),dir);
269 
270         // hook processes input to browser's output (async)
271         final InputStream inFromReq=req.getInputStream();
272         final OutputStream outToCgi=p.getOutputStream();
273         final int inLength=len;
274 
275         IO.copyThread(p.getErrorStream(),System.err);
276         
277         new Thread(new Runnable()
278         {
279             public void run()
280             {
281                 try
282                 {
283                     if (inLength>0)
284                         IO.copy(inFromReq,outToCgi,inLength);
285                     outToCgi.close();
286                 }
287                 catch (IOException e)
288                 {
289                     Log.ignore(e);
290                 }
291             }
292         }).start();
293 
294         // hook processes output to browser's input (sync)
295         // if browser closes stream, we should detect it and kill process...
296         OutputStream os = null;
297         try
298         {
299             // read any headers off the top of our input stream
300             // NOTE: Multiline header items not supported!
301             String line=null;
302             InputStream inFromCgi=p.getInputStream();
303 
304             //br=new BufferedReader(new InputStreamReader(inFromCgi));
305             //while ((line=br.readLine())!=null)
306             while( (line = getTextLineFromStream( inFromCgi )).length() > 0 )
307             {
308                 if (!line.startsWith("HTTP"))
309                 {
310                     int k=line.indexOf(':');
311                     if (k>0)
312                     {
313                         String key=line.substring(0,k).trim();
314                         String value = line.substring(k+1).trim();
315                         if ("Location".equals(key))
316                         {
317                             res.sendRedirect(value);
318                         }
319                         else if ("Status".equals(key))
320                         {
321                         	String[] token = value.split( " " );
322                             int status=Integer.parseInt(token[0]);
323                             res.setStatus(status);
324                         }
325                         else
326                         {
327                             // add remaining header items to our response header
328                             res.addHeader(key,value);
329                         }
330                     }
331                 }
332             }
333             // copy cgi content to response stream...
334             os = res.getOutputStream();
335             IO.copy(inFromCgi, os);
336             p.waitFor();
337 
338             if (!_ignoreExitState)
339             {
340                 int exitValue=p.exitValue();
341                 if (0!=exitValue)
342                 {
343                     Log.warn("Non-zero exit status ("+exitValue+") from CGI program: "+path);
344                     if (!res.isCommitted())
345                         res.sendError(500,"Failed to exec CGI");
346                 }
347             }
348         }
349         catch (IOException e)
350         {
351             // browser has probably closed its input stream - we
352             // terminate and clean up...
353             Log.debug("CGI: Client closed connection!");
354         }
355         catch (InterruptedException ie)
356         {
357             Log.debug("CGI: interrupted!");
358         }
359         finally
360         {
361             if( os != null )
362             {
363                 try
364                 {
365                     os.close();
366                 }
367             	catch(Exception e)
368             	{
369             	    Log.ignore(e);
370             	}
371             }
372             os = null;
373             p.destroy();
374             // Log.debug("CGI: terminated!");
375         }
376     }
377 
378     /**
379      * Utility method to get a line of text from the input stream.
380      * @param is the input stream
381      * @return the line of text
382      * @throws IOException
383      */
384     private String getTextLineFromStream( InputStream is ) throws IOException {
385         StringBuffer buffer = new StringBuffer();
386         int b;
387 
388        	while( (b = is.read()) != -1 && b != (int) '\n' ) {
389        		buffer.append( (char) b );
390        	}
391        	return buffer.toString().trim();
392     }
393     /* ------------------------------------------------------------ */
394     /**
395      * private utility class that manages the Environment passed to exec.
396      */
397     private static class EnvList
398     {
399         private Map envMap;
400 
401         EnvList()
402         {
403             envMap=new HashMap();
404         }
405 
406         EnvList(EnvList l)
407         {
408             envMap=new HashMap(l.envMap);
409         }
410 
411         /**
412          * Set a name/value pair, null values will be treated as an empty String
413          */
414         public void set(String name, String value)
415         {
416             envMap.put(name,name+"="+StringUtil.nonNull(value));
417         }
418 
419         /** Get representation suitable for passing to exec. */
420         public String[] getEnvArray()
421         {
422             return (String[])envMap.values().toArray(new String[envMap.size()]);
423         }
424 
425         public String toString()
426         {
427             return envMap.toString();
428         }
429     }
430 }