| // |
| // ======================================================================== |
| // Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.servlets; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Writer; |
| import java.nio.charset.Charset; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import javax.servlet.AsyncContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.eclipse.jetty.http.HttpMethod; |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.MultiMap; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.UrlEncoded; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * CGI Servlet. |
| * <p> |
| * The following init parameters are used to configure this servlet: |
| * <dl> |
| * <dt>cgibinResourceBase</dt> |
| * <dd>Path to the cgi bin directory if set or it will default to the resource base of the context.</dd> |
| * <dt>resourceBase</dt> |
| * <dd>An alias for cgibinResourceBase.</dd> |
| * <dt>cgibinResourceBaseIsRelative</dt> |
| * <dd>If true then cgibinResourceBase is relative to the webapp (eg "WEB-INF/cgi")</dd> |
| * <dt>commandPrefix</dt> |
| * <dd>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.</dd> |
| * <dt>Path</dt> |
| * <dd>passed to the exec environment as PATH.</dd> |
| * <dt>ENV_*</dt> |
| * <dd>used to set an arbitrary environment variable with the name stripped of the leading ENV_ and using the init parameter value</dd> |
| * <dt>useFullPath</dt> |
| * <dd>If true, the full URI path within the context is used for the exec command, otherwise a search is done for a partial URL that matches an exec Command</dd> |
| * <dt>ignoreExitState</dt> |
| * <dd>If true then do not act on a non-zero exec exit status")</dd> |
| * </dl> |
| */ |
| public class CGI extends HttpServlet |
| { |
| private static final long serialVersionUID = -6182088932884791074L; |
| |
| private static final Logger LOG = Log.getLogger(CGI.class); |
| |
| private boolean _ok; |
| private File _docRoot; |
| private boolean _cgiBinProvided; |
| private String _path; |
| private String _cmdPrefix; |
| private boolean _useFullPath; |
| private EnvList _env; |
| private boolean _ignoreExitState; |
| private boolean _relative; |
| |
| /* ------------------------------------------------------------ */ |
| @Override |
| public void init() throws ServletException |
| { |
| _env = new EnvList(); |
| _cmdPrefix = getInitParameter("commandPrefix"); |
| _useFullPath = Boolean.parseBoolean(getInitParameter("useFullPath")); |
| _relative = Boolean.parseBoolean(getInitParameter("cgibinResourceBaseIsRelative")); |
| |
| String tmp = getInitParameter("cgibinResourceBase"); |
| if (tmp != null) |
| _cgiBinProvided = true; |
| else |
| { |
| tmp = getInitParameter("resourceBase"); |
| if (tmp != null) |
| _cgiBinProvided = true; |
| else |
| tmp = getServletContext().getRealPath("/"); |
| } |
| |
| if (_relative && _cgiBinProvided) |
| { |
| tmp = getServletContext().getRealPath(tmp); |
| } |
| |
| if (tmp == null) |
| { |
| LOG.warn("CGI: no CGI bin !"); |
| return; |
| } |
| |
| File dir = new File(tmp); |
| if (!dir.exists()) |
| { |
| LOG.warn("CGI: CGI bin does not exist - " + dir); |
| return; |
| } |
| |
| if (!dir.canRead()) |
| { |
| LOG.warn("CGI: CGI bin is not readable - " + dir); |
| return; |
| } |
| |
| if (!dir.isDirectory()) |
| { |
| LOG.warn("CGI: CGI bin is not a directory - " + dir); |
| return; |
| } |
| |
| try |
| { |
| _docRoot = dir.getCanonicalFile(); |
| } |
| catch (IOException e) |
| { |
| LOG.warn("CGI: CGI bin failed - " + dir,e); |
| return; |
| } |
| |
| _path = getInitParameter("Path"); |
| if (_path != null) |
| _env.set("PATH",_path); |
| |
| _ignoreExitState = "true".equalsIgnoreCase(getInitParameter("ignoreExitState")); |
| Enumeration<String> e = getInitParameterNames(); |
| while (e.hasMoreElements()) |
| { |
| String n = e.nextElement(); |
| if (n != null && n.startsWith("ENV_")) |
| _env.set(n.substring(4),getInitParameter(n)); |
| } |
| if (!_env.envMap.containsKey("SystemRoot")) |
| { |
| String os = System.getProperty("os.name"); |
| if (os != null && os.toLowerCase(Locale.ENGLISH).indexOf("windows") != -1) |
| { |
| _env.set("SystemRoot","C:\\WINDOWS"); |
| } |
| } |
| |
| _ok = true; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| @Override |
| public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException |
| { |
| if (!_ok) |
| { |
| res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); |
| return; |
| } |
| |
| if (LOG.isDebugEnabled()) |
| { |
| LOG.debug("CGI: ContextPath : " + req.getContextPath()); |
| LOG.debug("CGI: ServletPath : " + req.getServletPath()); |
| LOG.debug("CGI: PathInfo : " + req.getPathInfo()); |
| LOG.debug("CGI: _docRoot : " + _docRoot); |
| LOG.debug("CGI: _path : " + _path); |
| LOG.debug("CGI: _ignoreExitState: " + _ignoreExitState); |
| } |
| |
| // pathInContext may actually comprises scriptName/pathInfo...We will |
| // walk backwards up it until we find the script - the rest must |
| // be the pathInfo; |
| String pathInContext = (_relative ? "" : StringUtil.nonNull(req.getServletPath())) + StringUtil.nonNull(req.getPathInfo()); |
| File execCmd = new File(_docRoot, pathInContext); |
| String pathInfo = pathInContext; |
| |
| if(!_useFullPath) |
| { |
| String path = pathInContext; |
| String info = ""; |
| |
| // Search docroot for a matching execCmd |
| while ((path.endsWith("/") || !execCmd.exists()) && path.length() >= 0) |
| { |
| int index = path.lastIndexOf('/'); |
| path = path.substring(0,index); |
| info = pathInContext.substring(index,pathInContext.length()); |
| execCmd = new File(_docRoot,path); |
| } |
| |
| if (path.length() == 0 || !execCmd.exists() || execCmd.isDirectory() || !execCmd.getCanonicalPath().equals(execCmd.getAbsolutePath())) |
| { |
| res.sendError(404); |
| } |
| |
| pathInfo = info; |
| } |
| exec(execCmd,pathInfo,req,res); |
| } |
| |
| /** executes the CGI process |
| /* |
| * @param command the command to execute, this command is prefixed by |
| * the context parameter "commandPrefix". |
| * @param pathInfo The PATH_INFO to process, |
| * see http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getPathInfo%28%29. Cannot be null |
| * @param req |
| * @param res |
| * @exception IOException |
| */ |
| private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException |
| { |
| assert req != null; |
| assert res != null; |
| assert pathInfo != null; |
| assert command != null; |
| |
| if (LOG.isDebugEnabled()) |
| { |
| LOG.debug("CGI: script is " + command); |
| LOG.debug("CGI: pathInfo is " + pathInfo); |
| } |
| |
| String bodyFormEncoded = null; |
| if ((HttpMethod.POST.equals(req.getMethod()) || HttpMethod.PUT.equals(req.getMethod())) && "application/x-www-form-urlencoded".equals(req.getContentType())) |
| { |
| MultiMap<String> parameterMap = new MultiMap<String>(); |
| Enumeration<String> names = req.getParameterNames(); |
| while (names.hasMoreElements()) |
| { |
| String parameterName = names.nextElement(); |
| parameterMap.addValues(parameterName, req.getParameterValues(parameterName)); |
| } |
| bodyFormEncoded = UrlEncoded.encode(parameterMap, Charset.forName(req.getCharacterEncoding()), true); |
| } |
| |
| 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()); |
| |
| int contentLen = req.getContentLength(); |
| if (contentLen < 0) |
| contentLen = 0; |
| if (bodyFormEncoded != null) |
| { |
| env.set("CONTENT_LENGTH", Integer.toString(bodyFormEncoded.length())); |
| } |
| else |
| { |
| env.set("CONTENT_LENGTH", Integer.toString(contentLen)); |
| } |
| env.set("CONTENT_TYPE", req.getContentType()); |
| env.set("GATEWAY_INTERFACE", "CGI/1.1"); |
| if (pathInfo.length() > 0) |
| { |
| env.set("PATH_INFO", pathInfo); |
| } |
| |
| String pathTranslated = req.getPathTranslated(); |
| if ((pathTranslated == null) || (pathTranslated.length() == 0)) |
| pathTranslated = 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()); |
| |
| String scriptPath; |
| String scriptName; |
| // use docRoot for scriptPath, too |
| if(_cgiBinProvided) |
| { |
| scriptPath = command.getAbsolutePath(); |
| scriptName = scriptPath.substring(_docRoot.getAbsolutePath().length()); |
| } |
| else |
| { |
| String requestURI = req.getRequestURI(); |
| scriptName = requestURI.substring(0,requestURI.length() - pathInfo.length()); |
| scriptPath = getServletContext().getRealPath(scriptName); |
| } |
| env.set("SCRIPT_FILENAME", scriptPath); |
| env.set("SCRIPT_NAME", 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<String> enm = req.getHeaderNames(); |
| while (enm.hasMoreElements()) |
| { |
| String name = enm.nextElement(); |
| String value = req.getHeader(name); |
| env.set("HTTP_" + name.toUpperCase(Locale.ENGLISH).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 absolutePath = command.getAbsolutePath(); |
| String execCmd = absolutePath; |
| |
| // escape the execCommand |
| if (execCmd.length() > 0 && execCmd.charAt(0) != '"' && execCmd.indexOf(" ") >= 0) |
| execCmd = "\"" + execCmd + "\""; |
| |
| if (_cmdPrefix != null) |
| execCmd = _cmdPrefix + " " + execCmd; |
| |
| assert execCmd != null; |
| LOG.debug("Environment: " + env.getExportString()); |
| LOG.debug("Command: " + execCmd); |
| |
| final Process p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), _docRoot); |
| |
| // hook processes input to browser's output (async) |
| if (bodyFormEncoded != null) |
| writeProcessInput(p, bodyFormEncoded); |
| else if (contentLen > 0) |
| writeProcessInput(p, req.getInputStream(), contentLen); |
| |
| // hook processes output to browser's input (sync) |
| // if browser closes stream, we should detect it and kill process... |
| OutputStream os = null; |
| AsyncContext async=req.startAsync(); |
| try |
| { |
| async.start(new Runnable() |
| { |
| @Override |
| public void run() |
| { |
| try |
| { |
| IO.copy(p.getErrorStream(), System.err); |
| } |
| catch (IOException e) |
| { |
| LOG.warn(e); |
| } |
| } |
| }); |
| |
| // 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(); |
| IO.copy(inFromCgi,os); |
| p.waitFor(); |
| |
| if (!_ignoreExitState) |
| { |
| int exitValue = p.exitValue(); |
| if (0 != exitValue) |
| { |
| LOG.warn("Non-zero exit status (" + exitValue + ") from CGI program: " + absolutePath); |
| 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!", e); |
| } |
| catch (InterruptedException ie) |
| { |
| LOG.debug("CGI: interrupted!"); |
| } |
| finally |
| { |
| if (os != null) |
| { |
| try |
| { |
| os.close(); |
| } |
| catch (Exception e) |
| { |
| LOG.debug(e); |
| } |
| } |
| p.destroy(); |
| // LOG.debug("CGI: terminated!"); |
| async.complete(); |
| } |
| } |
| |
| private static void writeProcessInput(final Process p, final String input) |
| { |
| new Thread(new Runnable() |
| { |
| @Override |
| public void run() |
| { |
| try |
| { |
| try (Writer outToCgi = new OutputStreamWriter(p.getOutputStream())) |
| { |
| outToCgi.write(input); |
| } |
| } |
| catch (IOException e) |
| { |
| LOG.debug(e); |
| } |
| } |
| }).start(); |
| } |
| |
| private static void writeProcessInput(final Process p, final InputStream input, final int len) |
| { |
| if (len <= 0) return; |
| |
| new Thread(new Runnable() |
| { |
| @Override |
| public void run() |
| { |
| try |
| { |
| OutputStream outToCgi = p.getOutputStream(); |
| IO.copy(input, outToCgi, len); |
| outToCgi.close(); |
| } |
| catch (IOException e) |
| { |
| LOG.debug(e); |
| } |
| } |
| }).start(); |
| } |
| |
| /** |
| * Utility method to get a line of text from the input stream. |
| * |
| * @param is |
| * the input stream |
| * @return the line of text |
| * @throws IOException |
| */ |
| private static String getTextLineFromStream(InputStream is) throws IOException |
| { |
| StringBuilder buffer = new StringBuilder(); |
| int b; |
| |
| while ((b = is.read()) != -1 && b != '\n') |
| { |
| buffer.append((char)b); |
| } |
| return buffer.toString().trim(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** |
| * private utility class that manages the Environment passed to exec. |
| */ |
| private static class EnvList |
| { |
| private Map<String, String> envMap; |
| |
| EnvList() |
| { |
| envMap = new HashMap<String, String>(); |
| } |
| |
| EnvList(EnvList l) |
| { |
| envMap = new HashMap<String,String>(l.envMap); |
| } |
| |
| /** |
| * Set a name/value pair, null values will be treated as an empty String |
| * @param name the name |
| * @param value the value |
| */ |
| public void set(String name, String value) |
| { |
| envMap.put(name,name + "=" + StringUtil.nonNull(value)); |
| } |
| |
| /** |
| * Get representation suitable for passing to exec. |
| * @return the env map as an array |
| */ |
| public String[] getEnvArray() |
| { |
| return envMap.values().toArray(new String[envMap.size()]); |
| } |
| |
| public String getExportString() |
| { |
| StringBuilder sb = new StringBuilder(); |
| for (String variable : getEnvArray()) |
| { |
| sb.append("export \""); |
| sb.append(variable); |
| sb.append("\"; "); |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return envMap.toString(); |
| } |
| } |
| } |