View Javadoc

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