// ======================================================================== // 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 net.lightbody.bmp.proxy.jetty.servlet; import net.lightbody.bmp.proxy.jetty.http.HttpFields; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import net.lightbody.bmp.proxy.jetty.util.IO; import net.lightbody.bmp.proxy.jetty.util.LineInput; import net.lightbody.bmp.proxy.jetty.util.LogSupport; import net.lightbody.bmp.proxy.jetty.util.StringUtil; import org.apache.commons.logging.Log; 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()]); } } }