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         	_env.set("SystemRoot", "C:\\WINDOWS"); 
131             }
132         }   
133       
134         _ok=true;
135     }
136 
137     /* ------------------------------------------------------------ */
138     public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
139     {
140         if (!_ok)
141         {
142             res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
143             return;
144         }
145         
146         String pathInContext=StringUtil.nonNull(req.getServletPath())+StringUtil.nonNull(req.getPathInfo());
147 
148         if (Log.isDebugEnabled())
149         {
150             Log.debug("CGI: ContextPath : "+req.getContextPath());
151             Log.debug("CGI: ServletPath : "+req.getServletPath());
152             Log.debug("CGI: PathInfo    : "+req.getPathInfo());
153             Log.debug("CGI: _docRoot    : "+_docRoot);
154             Log.debug("CGI: _path       : "+_path);
155             Log.debug("CGI: _ignoreExitState: "+_ignoreExitState);
156         }
157 
158         // pathInContext may actually comprises scriptName/pathInfo...We will
159         // walk backwards up it until we find the script - the rest must
160         // be the pathInfo;
161 
162         String both=pathInContext;
163         String first=both;
164         String last="";
165 
166         File exe=new File(_docRoot,first);
167 
168         while ((first.endsWith("/")||!exe.exists())&&first.length()>=0)
169         {
170             int index=first.lastIndexOf('/');
171 
172             first=first.substring(0,index);
173             last=both.substring(index,both.length());
174             exe=new File(_docRoot,first);
175         }
176 
177         if (first.length()==0||!exe.exists()||exe.isDirectory()||!exe.getCanonicalPath().equals(exe.getAbsolutePath()))
178         {
179             res.sendError(404);
180         }
181         else
182         {
183             if (Log.isDebugEnabled())
184             {
185                 Log.debug("CGI: script is "+exe);
186                 Log.debug("CGI: pathInfo is "+last);
187             }
188             exec(exe,last,req,res);
189         }
190     }
191 
192     /* ------------------------------------------------------------ */
193     /*
194      * @param root @param path @param req @param res @exception IOException
195      */
196     private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException
197     {
198         String path=command.getAbsolutePath();
199         File dir=command.getParentFile();
200         String scriptName=req.getRequestURI().substring(0,req.getRequestURI().length()-pathInfo.length());
201         String scriptPath=getServletContext().getRealPath(scriptName);
202         String pathTranslated=req.getPathTranslated();
203 
204         int len=req.getContentLength();
205         if (len<0)
206             len=0;
207         if ((pathTranslated==null)||(pathTranslated.length()==0))
208             pathTranslated=path;
209 
210         EnvList env=new EnvList(_env);
211         // these ones are from "The WWW Common Gateway Interface Version 1.1"
212         // look at :
213         // http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
214         env.set("AUTH_TYPE",req.getAuthType());
215         env.set("CONTENT_LENGTH",Integer.toString(len));
216         env.set("CONTENT_TYPE",req.getContentType());
217         env.set("GATEWAY_INTERFACE","CGI/1.1");
218         if ((pathInfo!=null)&&(pathInfo.length()>0))
219         {
220             env.set("PATH_INFO",pathInfo);
221         }
222         env.set("PATH_TRANSLATED",pathTranslated);
223         env.set("QUERY_STRING",req.getQueryString());
224         env.set("REMOTE_ADDR",req.getRemoteAddr());
225         env.set("REMOTE_HOST",req.getRemoteHost());
226         // The identity information reported about the connection by a
227         // RFC 1413 [11] request to the remote agent, if
228         // available. Servers MAY choose not to support this feature, or
229         // not to request the data for efficiency reasons.
230         // "REMOTE_IDENT" => "NYI"
231         env.set("REMOTE_USER",req.getRemoteUser());
232         env.set("REQUEST_METHOD",req.getMethod());
233         env.set("SCRIPT_NAME",scriptName);
234         env.set("SCRIPT_FILENAME",scriptPath);
235         env.set("SERVER_NAME",req.getServerName());
236         env.set("SERVER_PORT",Integer.toString(req.getServerPort()));
237         env.set("SERVER_PROTOCOL",req.getProtocol());
238         env.set("SERVER_SOFTWARE",getServletContext().getServerInfo());
239 
240         Enumeration enm=req.getHeaderNames();
241         while (enm.hasMoreElements())
242         {
243             String name=(String)enm.nextElement();
244             String value=req.getHeader(name);
245             env.set("HTTP_"+name.toUpperCase().replace('-','_'),value);
246         }
247 
248         // these extra ones were from printenv on www.dev.nomura.co.uk
249         env.set("HTTPS",(req.isSecure()?"ON":"OFF"));
250         // "DOCUMENT_ROOT" => root + "/docs",
251         // "SERVER_URL" => "NYI - http://us0245",
252         // "TZ" => System.getProperty("user.timezone"),
253 
254         // are we meant to decode args here ? or does the script get them
255         // via PATH_INFO ? if we are, they should be decoded and passed
256         // into exec here...
257         String execCmd=path;
258         if ((execCmd.charAt(0)!='"')&&(execCmd.indexOf(" ")>=0))
259             execCmd="\""+execCmd+"\"";
260         if (_cmdPrefix!=null)
261             execCmd=_cmdPrefix+" "+execCmd;
262 
263         Process p=(dir==null)?Runtime.getRuntime().exec(execCmd,env.getEnvArray()):Runtime.getRuntime().exec(execCmd,env.getEnvArray(),dir);
264 
265         // hook processes input to browser's output (async)
266         final InputStream inFromReq=req.getInputStream();
267         final OutputStream outToCgi=p.getOutputStream();
268         final int inLength=len;
269 
270         IO.copyThread(p.getErrorStream(),System.err);
271         
272         new Thread(new Runnable()
273         {
274             public void run()
275             {
276                 try
277                 {
278                     if (inLength>0)
279                         IO.copy(inFromReq,outToCgi,inLength);
280                     outToCgi.close();
281                 }
282                 catch (IOException e)
283                 {
284                     Log.ignore(e);
285                 }
286             }
287         }).start();
288 
289         // hook processes output to browser's input (sync)
290         // if browser closes stream, we should detect it and kill process...
291         OutputStream os = null;
292         try
293         {
294             // read any headers off the top of our input stream
295             // NOTE: Multiline header items not supported!
296             String line=null;
297             InputStream inFromCgi=p.getInputStream();
298 
299             //br=new BufferedReader(new InputStreamReader(inFromCgi));
300             //while ((line=br.readLine())!=null)
301             while( (line = getTextLineFromStream( inFromCgi )).length() > 0 )
302             {
303                 if (!line.startsWith("HTTP"))
304                 {
305                     int k=line.indexOf(':');
306                     if (k>0)
307                     {
308                         String key=line.substring(0,k).trim();
309                         String value = line.substring(k+1).trim();
310                         if ("Location".equals(key))
311                         {
312                             res.sendRedirect(res.encodeRedirectURL(value));
313                         }
314                         else if ("Status".equals(key))
315                         {
316                         	String[] token = value.split( " " );
317                             int status=Integer.parseInt(token[0]);
318                             res.setStatus(status);
319                         }
320                         else
321                         {
322                             // add remaining header items to our response header
323                             res.addHeader(key,value);
324                         }
325                     }
326                 }
327             }
328             // copy cgi content to response stream...
329             os = res.getOutputStream();
330             IO.copy(inFromCgi, os);
331             p.waitFor();
332 
333             if (!_ignoreExitState)
334             {
335                 int exitValue=p.exitValue();
336                 if (0!=exitValue)
337                 {
338                     Log.warn("Non-zero exit status ("+exitValue+") from CGI program: "+path);
339                     if (!res.isCommitted())
340                         res.sendError(500,"Failed to exec CGI");
341                 }
342             }
343         }
344         catch (IOException e)
345         {
346             // browser has probably closed its input stream - we
347             // terminate and clean up...
348             Log.debug("CGI: Client closed connection!");
349         }
350         catch (InterruptedException ie)
351         {
352             Log.debug("CGI: interrupted!");
353         }
354         finally
355         {
356             if( os != null )
357             {
358                 try
359                 {
360                     os.close();
361                 }
362                 catch(Exception e)
363                 {
364                     Log.ignore(e);
365                 }
366             }
367             os = null;
368             p.destroy();
369             // Log.debug("CGI: terminated!");
370         }
371     }
372 
373     /**
374      * Utility method to get a line of text from the input stream.
375      * @param is the input stream
376      * @return the line of text
377      * @throws IOException
378      */
379     private String getTextLineFromStream( InputStream is ) throws IOException {
380         StringBuilder buffer = new StringBuilder();
381         int b;
382 
383        	while( (b = is.read()) != -1 && b != (int) '\n' ) {
384        		buffer.append( (char) b );
385        	}
386        	return buffer.toString().trim();
387     }
388     /* ------------------------------------------------------------ */
389     /**
390      * private utility class that manages the Environment passed to exec.
391      */
392     private static class EnvList
393     {
394         private Map envMap;
395 
396         EnvList()
397         {
398             envMap=new HashMap();
399         }
400 
401         EnvList(EnvList l)
402         {
403             envMap=new HashMap(l.envMap);
404         }
405 
406         /**
407          * Set a name/value pair, null values will be treated as an empty String
408          */
409         public void set(String name, String value)
410         {
411             envMap.put(name,name+"="+StringUtil.nonNull(value));
412         }
413 
414         /** Get representation suitable for passing to exec. */
415         public String[] getEnvArray()
416         {
417             return (String[])envMap.values().toArray(new String[envMap.size()]);
418         }
419 
420         public String toString()
421         {
422             return envMap.toString();
423         }
424     }
425 }