View Javadoc

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