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