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