/src/main/java/org/browsermob/proxy/jetty/servlet/CGI.java
Java | 378 lines | 247 code | 52 blank | 79 comment | 41 complexity | a368755caa198df08b23b9dc41d6d562 MD5 | raw file
- // ========================================================================
- // A very basic CGI Servlet, for use, with Jetty
- // (jetty.jetty.org). It's heading towards CGI/1.1 compliance, but
- // still lacks a few features - the basic stuff is here though...
- // Copyright 2000 Julian Gosnell <jules_gosnell@yahoo.com> Released
- // under the terms of the Jetty Licence.
- // ========================================================================
- // TODO
- // - logging
- // - child's stderr
- // - exceptions should report to client via sendError()
- // - tidy up
- package org.browsermob.proxy.jetty.servlet;
- import org.apache.commons.logging.Log;
- import org.browsermob.proxy.jetty.http.HttpFields;
- import org.browsermob.proxy.jetty.log.LogFactory;
- import org.browsermob.proxy.jetty.util.IO;
- import org.browsermob.proxy.jetty.util.LineInput;
- import org.browsermob.proxy.jetty.util.LogSupport;
- import org.browsermob.proxy.jetty.util.StringUtil;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.util.Enumeration;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map;
- //-----------------------------------------------------------------------------
- /** CGI Servlet.
- *
- * The cgi bin directory can be set with the cgibinResourceBase init
- * parameter or it will default to the resource base of the context.
- *
- * 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 particular file type. For example on windows this can be set
- * to "perl" so that perl scripts are executed.
- *
- * The "Path" init param is passed to the exec environment as PATH.
- * Note: Must be run unpacked somewhere in the filesystem.
- *
- * 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.
- *
- * @version $Revision: 1.27 $
- * @author Julian Gosnell
- */
- public class CGI extends HttpServlet
- {
- private static Log log = LogFactory.getLog(CGI.class);
- protected File _docRoot;
- protected String _path;
- protected String _cmdPrefix;
- protected EnvList _env;
- /* ------------------------------------------------------------ */
- public void init()
- throws ServletException
- {
- _env= new EnvList();
- _cmdPrefix=getInitParameter("commandPrefix");
- String tmp = getInitParameter("cgibinResourceBase");
- if (tmp==null)
- tmp = getServletContext().getRealPath("/");
- if(log.isDebugEnabled())log.debug("CGI: CGI bin "+tmp);
- if (tmp==null)
- {
- log.warn("CGI: no CGI bin !");
- throw new ServletException();
- }
- File dir = new File(tmp);
- if (!dir.exists())
- {
- log.warn("CGI: CGI bin does not exist - "+dir);
- throw new ServletException();
- }
- if (!dir.canRead())
- {
- log.warn("CGI: CGI bin is not readable - "+dir);
- throw new ServletException();
- }
- if (!dir.isDirectory())
- {
- log.warn("CGI: CGI bin is not a directory - "+dir);
- throw new ServletException();
- }
- try
- {
- _docRoot=dir.getCanonicalFile();
- if(log.isDebugEnabled())log.debug("CGI: CGI bin accepted - "+_docRoot);
- }
- catch (IOException e)
- {
- log.warn("CGI: CGI bin failed - "+dir);
- e.printStackTrace();
- throw new ServletException();
- }
- _path=getInitParameter("Path");
- if(log.isDebugEnabled())log.debug("CGI: PATH accepted - "+_path);
- if (_path != null)
- _env.set("PATH", _path);
- Enumeration e= getInitParameterNames();
- while (e.hasMoreElements())
- {
- String n= (String)e.nextElement();
- if (n != null && n.startsWith("ENV_"))
- _env.set(n.substring(4),getInitParameter(n));
- }
- }
- /* ------------------------------------------------------------ */
- public void service(HttpServletRequest req, HttpServletResponse res)
- throws ServletException, IOException
- {
- String pathInContext =
- StringUtil.nonNull(req.getServletPath()) +
- StringUtil.nonNull(req.getPathInfo());
- if(log.isDebugEnabled())log.debug("CGI: req.getContextPath() : "+req.getContextPath());
- if(log.isDebugEnabled())log.debug("CGI: req.getServletPath() : "+req.getServletPath());
- if(log.isDebugEnabled())log.debug("CGI: req.getPathInfo() : "+req.getPathInfo());
- if(log.isDebugEnabled())log.debug("CGI: _docRoot : "+_docRoot);
- // pathInContext may actually comprises scriptName/pathInfo...We will
- // walk backwards up it until we find the script - the rest must
- // be the pathInfo;
- String both=pathInContext;
- String first=both;
- String last="";
- File exe=new File(_docRoot, first);
- while ((first.endsWith("/") || !exe.exists()) && first.length()>=0)
- {
- int index=first.lastIndexOf('/');
- first=first.substring(0, index);
- last=both.substring(index, both.length());
- exe=new File(_docRoot, first);
- }
- if (first.length()==0 ||
- !exe.exists() ||
- !exe.getCanonicalPath().equals(exe.getAbsolutePath()) ||
- exe.isDirectory())
- res.sendError(404);
- else
- {
- if(log.isDebugEnabled())log.debug("CGI: script is "+exe);
- if(log.isDebugEnabled())log.debug("CGI: pathInfo is "+last);
- exec(exe, last, req, res);
- }
- }
- /* ------------------------------------------------------------ */
- /*
- * @param root
- * @param path
- * @param req
- * @param res
- * @exception IOException
- */
- private void exec(File command,
- String pathInfo,
- HttpServletRequest req,
- HttpServletResponse res)
- throws IOException
- {
- String path=command.toString();
- File dir=command.getParentFile();
- if(log.isDebugEnabled())log.debug("CGI: execing: "+path);
- EnvList env = new EnvList(_env);
- // these ones are from "The WWW Common Gateway Interface Version 1.1"
- // look at : http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
- env.set("AUTH_TYPE", req.getAuthType());
- env.set("CONTENT_LENGTH", Integer.toString(req.getContentLength()));
- env.set("CONTENT_TYPE", req.getContentType());
- env.set("GATEWAY_INTERFACE", "CGI/1.1");
- env.set("PATH_INFO", pathInfo);
- env.set("PATH_TRANSLATED", req.getPathTranslated());
- env.set("QUERY_STRING", req.getQueryString());
- env.set("REMOTE_ADDR", req.getRemoteAddr());
- env.set("REMOTE_HOST", req.getRemoteHost());
- // The identity information reported about the connection by a
- // RFC 1413 [11] request to the remote agent, if
- // available. Servers MAY choose not to support this feature, or
- // not to request the data for efficiency reasons.
- // "REMOTE_IDENT" => "NYI"
- env.set("REMOTE_USER", req.getRemoteUser());
- env.set("REQUEST_METHOD", req.getMethod());
- String scriptName = req.getRequestURI().substring(0,req.getRequestURI().length() - pathInfo.length());
- env.set("SCRIPT_NAME",scriptName);
- env.set("SCRIPT_FILENAME",getServletContext().getRealPath(scriptName));
- env.set("SERVER_NAME", req.getServerName());
- env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
- env.set("SERVER_PROTOCOL", req.getProtocol());
- env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
- Enumeration enm = req.getHeaderNames();
- while (enm.hasMoreElements())
- {
- String name = (String) enm.nextElement();
- String value = req.getHeader(name);
- env.set("HTTP_" + name.toUpperCase().replace( '-', '_' ), value);
- }
- // these extra ones were from printenv on www.dev.nomura.co.uk
- env.set("HTTPS", (req.isSecure()?"ON":"OFF"));
- // "DOCUMENT_ROOT" => root + "/docs",
- // "SERVER_URL" => "NYI - http://us0245",
- // "TZ" => System.getProperty("user.timezone"),
- // are we meant to decode args here ? or does the script get them
- // via PATH_INFO ? if we are, they should be decoded and passed
- // into exec here...
- String execCmd=path;
- if (execCmd.indexOf(" ")>=0)
- execCmd="\""+execCmd+"\"";
- if (_cmdPrefix!=null)
- execCmd=_cmdPrefix+" "+execCmd;
- Process p=dir==null
- ?Runtime.getRuntime().exec(execCmd, env.getEnvArray())
- :Runtime.getRuntime().exec(execCmd, env.getEnvArray(),dir);
- // hook processes input to browser's output (async)
- final InputStream inFromReq=req.getInputStream();
- final OutputStream outToCgi=p.getOutputStream();
- final int inputLength = req.getContentLength();
- new Thread(new Runnable()
- {
- public void run()
- {
- try{
- if (inputLength>0)
- IO.copy(inFromReq,outToCgi,inputLength);
- outToCgi.close();
- }
- catch(IOException e){LogSupport.ignore(log,e);}
- }
- }).start();
- // hook processes output to browser's input (sync)
- // if browser closes stream, we should detect it and kill process...
- try
- {
- // read any headers off the top of our input stream
- LineInput li = new LineInput(p.getInputStream());
- HttpFields fields=new HttpFields();
- fields.read(li);
- String ContentStatus = "Status";
- String redirect = fields.get(HttpFields.__Location);
- String status = fields.get(ContentStatus);
- if (status!=null)
- {
- log.debug("Found a Status header - setting status on response");
- fields.remove(ContentStatus);
- // NOTE: we ignore any reason phrase, otherwise we
- // would need to use res.sendError() selectively.
- int i = status.indexOf(' ');
- if (i>0)
- status = status.substring(0,i);
- res.setStatus(Integer.parseInt(status));
- }
- // copy remaining headers into response...
- for (Iterator i=fields.iterator(); i.hasNext();)
- {
- HttpFields.Entry e=(HttpFields.Entry)i.next();
- res.addHeader(e.getKey(),e.getValue());
- }
- if (status==null && redirect != null)
- {
- // The CGI has set Location and is counting on us to do the redirect.
- // See http://CGI-Spec.Golux.Com/draft-coar-cgi-v11-03-clean.html#7.2.1.2
- if (!redirect.startsWith("http:/")&&!redirect.startsWith("https:/"))
- res.sendRedirect(redirect);
- else
- res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
- }
- // copy remains of input onto output...
- IO.copy(li, res.getOutputStream());
- p.waitFor();
- int exitValue = p.exitValue();
- if(log.isDebugEnabled())log.debug("CGI: p.exitValue(): " + exitValue);
- if (0 != exitValue)
- {
- log.warn("Non-zero exit status ("+exitValue+
- ") from CGI program: "+path);
- if (!res.isCommitted())
- res.sendError(500, "Failed to exec CGI");
- }
- }
- catch (IOException e)
- {
- // browser has probably closed its input stream - we
- // terminate and clean up...
- log.debug("CGI: Client closed connection!");
- }
- catch (InterruptedException ie)
- {
- log.debug("CGI: interrupted!");
- }
- finally
- {
- p.destroy();
- }
- if(log.isDebugEnabled())log.debug("CGI: Finished exec: " + p);
- }
- /* ------------------------------------------------------------ */
- /** private utility class that manages the Environment passed
- * to exec.
- */
- private static class EnvList
- {
- private Map envMap;
- EnvList()
- {
- envMap= new HashMap();
- }
- EnvList(EnvList l)
- {
- envMap= new HashMap(l.envMap);
- }
- /** Set a name/value pair, null values will be treated as
- * an empty String */
- public void set(String name, String value) {
- envMap.put(name, name + "=" + StringUtil.nonNull(value));
- }
- /** Get representation suitable for passing to exec. */
- public String[] getEnvArray()
- {
- return (String[])envMap.values().toArray(new String[envMap.size()]);
- }
- }
- }