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