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