View Javadoc

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