View Javadoc

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