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