From 4e238edec0148317cc74250e1c52ba56358c5f14 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 28 Sep 2010 18:58:50 +0200 Subject: [PATCH] added new cgiservlet based on jetty implementation --- .../java/sonia/scm/web/cgi/CGIRunner.java | 382 ++++++++++++++++++ .../java/sonia/scm/web/cgi/CGIServlet.java | 127 ++++++ .../main/java/sonia/scm/web/cgi/EnvList.java | 94 +++++ 3 files changed, 603 insertions(+) create mode 100644 scm-web-api/src/main/java/sonia/scm/web/cgi/CGIRunner.java create mode 100644 scm-web-api/src/main/java/sonia/scm/web/cgi/CGIServlet.java create mode 100644 scm-web-api/src/main/java/sonia/scm/web/cgi/EnvList.java diff --git a/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIRunner.java b/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIRunner.java new file mode 100644 index 0000000000..7aac6e9bb7 --- /dev/null +++ b/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIRunner.java @@ -0,0 +1,382 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.web.cgi; + +//~--- non-JDK imports -------------------------------------------------------- + +import sonia.scm.util.Util; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Based on org.eclipse.jetty.servlets.CGI + * + * @author Sebastian Sdorra + * + */ +public class CGIRunner +{ + + /** Field description */ + public static final int BUFFERSIZE = 2 * 8192; + + /** Field description */ + private static final Logger logger = + Logger.getLogger(CGIRunner.class.getName()); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param context + * @param environment + * @param cmdPrefix + * @param ignoreExitState + */ + public CGIRunner(ServletContext context, EnvList environment, + String cmdPrefix, boolean ignoreExitState) + { + this.context = context; + this.environment = environment; + this.cmdPrefix = cmdPrefix; + this.ignoreExitState = ignoreExitState; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param command + * @param pathInfo + * @param req + * @param res + * + * @throws IOException + */ + public void exec(File command, String pathInfo, HttpServletRequest req, + HttpServletResponse res) + throws IOException + { + String path = command.getAbsolutePath(); + File dir = command.getParentFile(); + String scriptName = req.getRequestURI().substring(0, + req.getRequestURI().length() - pathInfo.length()); + String scriptPath = context.getRealPath(scriptName); + String pathTranslated = req.getPathTranslated(); + int len = req.getContentLength(); + + if (len < 0) + { + len = 0; + } + + if ((pathTranslated == null) || (pathTranslated.length() == 0)) + { + pathTranslated = path; + } + + EnvList env = new EnvList(environment); + + // 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(len)); + env.set("CONTENT_TYPE", req.getContentType()); + env.set("GATEWAY_INTERFACE", "CGI/1.1"); + + if ((pathInfo != null) && (pathInfo.length() > 0)) + { + env.set("PATH_INFO", pathInfo); + } + + env.set("PATH_TRANSLATED", pathTranslated); + 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()); + env.set("SCRIPT_NAME", scriptName); + env.set("SCRIPT_FILENAME", scriptPath); + env.set("SERVER_NAME", req.getServerName()); + env.set("SERVER_PORT", Integer.toString(req.getServerPort())); + env.set("SERVER_PROTOCOL", req.getProtocol()); + env.set("SERVER_SOFTWARE", context.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.charAt(0) != '"') && (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 inLength = len; + + // TODO: print or log error stream + // IO.copyThread(p.getErrorStream(), System.err); + new Thread(new Runnable() + { + @Override + public void run() + { + try + { + if (inLength > 0) + { + copy(inFromReq, outToCgi, inLength); + } + + outToCgi.close(); + } + catch (IOException e) + { + logger.log(Level.FINEST, null, e); + } + } + }).start(); + + // hook processes output to browser's input (sync) + // if browser closes stream, we should detect it and kill process... + OutputStream os = null; + + try + { + + // read any headers off the top of our input stream + // NOTE: Multiline header items not supported! + String line = null; + InputStream inFromCgi = p.getInputStream(); + + // br=new BufferedReader(new InputStreamReader(inFromCgi)); + // while ((line=br.readLine())!=null) + while ((line = getTextLineFromStream(inFromCgi)).length() > 0) + { + if (!line.startsWith("HTTP")) + { + int k = line.indexOf(':'); + + if (k > 0) + { + String key = line.substring(0, k).trim(); + String value = line.substring(k + 1).trim(); + + if ("Location".equals(key)) + { + res.sendRedirect(res.encodeRedirectURL(value)); + } + else if ("Status".equals(key)) + { + String[] token = value.split(" "); + int status = Integer.parseInt(token[0]); + + res.setStatus(status); + } + else + { + + // add remaining header items to our response header + res.addHeader(key, value); + } + } + } + } + + // copy cgi content to response stream... + os = res.getOutputStream(); + Util.copy(inFromCgi, os); + p.waitFor(); + + if (!ignoreExitState) + { + int exitValue = p.exitValue(); + + if (0 != exitValue) + { + StringBuilder msg = new StringBuilder("Non-zero exit status ("); + + msg.append(exitValue).append(") from CGI program: ").append(path); + logger.warning(msg.toString()); + + 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... + logger.finest("CGI: Client closed connection!"); + } + catch (InterruptedException ie) + { + logger.finest("CGI: interrupted!"); + } + finally + { + if (os != null) + { + Util.close(os); + } + + os = null; + p.destroy(); + + // Log.debug("CGI: terminated!"); + } + } + + /** + * Method description + * + * + * @param in + * @param out + * @param byteCount + * + * @throws IOException + */ + private void copy(InputStream in, OutputStream out, long byteCount) + throws IOException + { + byte buffer[] = new byte[BUFFERSIZE]; + int len = BUFFERSIZE; + + if (byteCount >= 0) + { + while (byteCount > 0) + { + int max = (byteCount < BUFFERSIZE) + ? (int) byteCount + : BUFFERSIZE; + + len = in.read(buffer, 0, max); + + if (len == -1) + { + break; + } + + byteCount -= len; + out.write(buffer, 0, len); + } + } + else + { + while (true) + { + len = in.read(buffer, 0, BUFFERSIZE); + + if (len < 0) + { + break; + } + + out.write(buffer, 0, len); + } + } + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param is + * + * @return + * + * @throws IOException + */ + private String getTextLineFromStream(InputStream is) throws IOException + { + StringBuilder buffer = new StringBuilder(); + int b; + + while ((b = is.read()) != -1 && (b != (int) '\n')) + { + buffer.append((char) b); + } + + return buffer.toString().trim(); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private String cmdPrefix; + + /** Field description */ + private ServletContext context; + + /** Field description */ + private EnvList environment; + + /** Field description */ + private boolean ignoreExitState; +} diff --git a/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIServlet.java b/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIServlet.java new file mode 100644 index 0000000000..04830d2a5c --- /dev/null +++ b/scm-web-api/src/main/java/sonia/scm/web/cgi/CGIServlet.java @@ -0,0 +1,127 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.web.cgi; + +//~--- non-JDK imports -------------------------------------------------------- + +import sonia.scm.util.Util; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.IOException; + +import java.util.Enumeration; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 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. + * + * Based on org.eclipse.jetty.servlets.CGI + * + * @author Sebastian Sdorra + * + */ +public class CGIServlet extends HttpServlet +{ + + /** Field description */ + private static final long serialVersionUID = 5719539505555835833L; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @throws ServletException + */ + @Override + public void init() throws ServletException + { + EnvList env = new EnvList(); + String cmdPrefix = getInitParameter("commandPrefix"); + boolean ignoreExitStatus = + "true".equalsIgnoreCase(getInitParameter("ignoreExitState")); + String commandPath = getInitParameter("command"); + + if (Util.isNotEmpty(commandPath)) + { + command = new File(commandPath); + } + + Enumeration e = getInitParameterNames(); + + while (e.hasMoreElements()) + { + String n = (String) e.nextElement(); + + if ((n != null) && n.startsWith("ENV_")) + { + env.set(n.substring(4), getInitParameter(n)); + } + } + + if (!env.containsKey("SystemRoot")) + { + String os = System.getProperty("os.name"); + + if ((os != null) && (os.toLowerCase().indexOf("windows") != -1)) + { + env.set("SystemRoot", "C:\\WINDOWS"); + } + } + + cgiRunner = new CGIRunner(getServletContext(), env, cmdPrefix, + ignoreExitStatus); + } + + /** + * Method description + * + * + * @param req + * @param resp + * + * @throws IOException + * @throws ServletException + */ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + cgiRunner.exec(command, req.getPathInfo(), req, resp); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private CGIRunner cgiRunner; + + /** Field description */ + private File command; +} diff --git a/scm-web-api/src/main/java/sonia/scm/web/cgi/EnvList.java b/scm-web-api/src/main/java/sonia/scm/web/cgi/EnvList.java new file mode 100644 index 0000000000..abbdccea10 --- /dev/null +++ b/scm-web-api/src/main/java/sonia/scm/web/cgi/EnvList.java @@ -0,0 +1,94 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.web.cgi; + +//~--- non-JDK imports -------------------------------------------------------- + +import sonia.scm.util.Util; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Sebastian Sdorra + */ +public class EnvList +{ + + /** + * Constructs ... + * + */ + EnvList() + { + envMap = new HashMap(); + } + + /** + * Constructs ... + * + * + * @param l + */ + EnvList(EnvList l) + { + envMap = new HashMap(l.envMap); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @return + */ + @Override + public String toString() + { + return envMap.toString(); + } + + public boolean containsKey( String key ) + { + return envMap.containsKey(key); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Get representation suitable for passing to exec. + * + * @return + */ + public String[] getEnvArray() + { + return envMap.values().toArray(new String[envMap.size()]); + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Set a name/value pair, null values will be treated as an empty String + * + * @param name + * @param value + */ + public void set(String name, String value) + { + envMap.put(name, name.concat("=").concat(Util.nonNull(value))); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private Map envMap; +}