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