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