View Javadoc

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