CGIServlet.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.servlets;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apache.catalina.util.IOTools;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.compat.JrePlatform;
import org.apache.tomcat.util.res.StringManager;
/**
* CGI-invoking servlet for web applications, used to execute scripts which comply to the Common Gateway Interface (CGI)
* specification and are named in the path-info used to invoke this servlet.
* <p>
* <i>Note: This code compiles and even works for simple CGI cases. Exhaustive testing has not been done. Please
* consider it beta quality. Feedback is appreciated to the author (see below).</i>
* </p>
* <p>
* <b>Example</b>:<br>
* If an instance of this servlet was mapped (using <code><web-app>/WEB-INF/web.xml</code>) to:
* </p>
* <p>
* <code>
* <web-app>/cgi-bin/*
* </code>
* </p>
* <p>
* then the following request:
* </p>
* <p>
* <code>
* http://localhost:8080/<web-app>/cgi-bin/dir1/script/pathinfo1
* </code>
* </p>
* <p>
* would result in the execution of the script
* </p>
* <p>
* <code>
* <web-app-root>/WEB-INF/cgi/dir1/script
* </code>
* </p>
* <p>
* with the script's <code>PATH_INFO</code> set to <code>/pathinfo1</code>.
* </p>
* <p>
* Recommendation: House all your CGI scripts under <code><webapp>/WEB-INF/cgi</code>. This will ensure that you
* do not accidentally expose your cgi scripts' code to the outside world and that your cgis will be cleanly ensconced
* underneath the WEB-INF (i.e., non-content) area.
* </p>
* <p>
* The default CGI location is mentioned above. You have the flexibility to put CGIs wherever you want, however:
* </p>
* <p>
* The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if
* cgiPathPrefix is null).
* </p>
* <p>
* cgiPathPrefix is defined by setting this servlet's cgiPathPrefix init parameter
* </p>
* <p>
* <B>CGI Specification</B>:<br>
* derived from <a href="http://cgi-spec.golux.com">http://cgi-spec.golux.com</a>. A work-in-progress & expired
* Internet Draft. Note no actual RFC describing the CGI specification exists. Where the behavior of this servlet
* differs from the specification cited above, it is either documented here, a bug, or an instance where the
* specification cited differs from Best Community Practice (BCP). Such instances should be well-documented here. Please
* email the <a href="https://tomcat.apache.org/lists.html">Tomcat group</a> with amendments.
* </p>
* <p>
* <b>Canonical metavariables</b>:<br>
* The CGI specification defines the following canonical metavariables: <br>
* [excerpt from CGI specification]
*
* <PRE>
* AUTH_TYPE
* CONTENT_LENGTH
* CONTENT_TYPE
* GATEWAY_INTERFACE
* PATH_INFO
* PATH_TRANSLATED
* QUERY_STRING
* REMOTE_ADDR
* REMOTE_HOST
* REMOTE_IDENT
* REMOTE_USER
* REQUEST_METHOD
* SCRIPT_NAME
* SERVER_NAME
* SERVER_PORT
* SERVER_PROTOCOL
* SERVER_SOFTWARE
* </PRE>
* <p>
* Metavariables with names beginning with the protocol name (<EM>e.g.</EM>, "HTTP_ACCEPT") are also canonical in their
* description of request header fields. The number and meaning of these fields may change independently of this
* specification. (See also section 6.1.5 [of the CGI specification].)
* </p>
* [end excerpt]
* <h2>Implementation notes</h2>
* <p>
* <b>standard input handling</b>: If your script accepts standard input, then the client must start sending input
* within a certain timeout period, otherwise the servlet will assume no input is coming and carry on running the
* script. The script's the standard input will be closed and handling of any further input from the client is
* undefined. Most likely it will be ignored. If this behavior becomes undesirable, then this servlet needs to be
* enhanced to handle threading of the spawned process' stdin, stdout, and stderr (which should not be too hard). <br>
* If you find your cgi scripts are timing out receiving input, you can set the init parameter
* <code>stderrTimeout</code> of your webapps' cgi-handling servlet.
* </p>
* <p>
* <b>Metavariable Values</b>: According to the CGI specification, implementations may choose to represent both null or
* missing values in an implementation-specific manner, but must define that manner. This implementation chooses to
* always define all required metavariables, but set the value to "" for all metavariables whose value is either null or
* undefined. PATH_TRANSLATED is the sole exception to this rule, as per the CGI Specification.
* </p>
* <p>
* <b>NPH -- Non-parsed-header implementation</b>: This implementation does not support the CGI NPH concept, whereby
* server ensures that the data supplied to the script are precisely as supplied by the client and unaltered by the
* server.
* </p>
* <p>
* The function of a servlet container (including Tomcat) is specifically designed to parse and possible alter
* CGI-specific variables, and as such makes NPH functionality difficult to support.
* </p>
* <p>
* The CGI specification states that compliant servers MAY support NPH output. It does not state servers MUST support
* NPH output to be unconditionally compliant. Thus, this implementation maintains unconditional compliance with the
* specification though NPH support is not present.
* </p>
* <p>
* The CGI specification is located at <a href="http://cgi-spec.golux.com">http://cgi-spec.golux.com</a>.
* </p>
* <h3>TODO:</h3>
* <ul>
* <li>Support for setting headers (for example, Location headers don't work)
* <li>Support for collapsing multiple header lines (per RFC 2616)
* <li>Ensure handling of POST method does not interfere with 2.3 Filters
* <li>Refactor some debug code out of core
* <li>Ensure header handling preserves encoding
* <li>Possibly rewrite CGIRunner.run()?
* <li>Possibly refactor CGIRunner and CGIEnvironment as non-inner classes?
* <li>Document handling of cgi stdin when there is no stdin
* <li>Revisit IOException handling in CGIRunner.run()
* <li>Better documentation
* <li>Confirm use of ServletInputStream.available() in CGIRunner.run() is not needed
* <li>[add more to this TODO list]
* </ul>
*
* @author Martin T Dengler [root@martindengler.com]
* @author Amy Roh
*/
public final class CGIServlet extends HttpServlet {
private static final Log log = LogFactory.getLog(CGIServlet.class);
private static final StringManager sm = StringManager.getManager(CGIServlet.class);
/* some vars below copied from Craig R. McClanahan's InvokerServlet */
private static final long serialVersionUID = 1L;
private static final Set<String> DEFAULT_SUPER_METHODS = new HashSet<>();
private static final Pattern DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN;
private static final String ALLOW_ANY_PATTERN = ".*";
static {
DEFAULT_SUPER_METHODS.add("HEAD");
DEFAULT_SUPER_METHODS.add("OPTIONS");
DEFAULT_SUPER_METHODS.add("TRACE");
if (JrePlatform.IS_WINDOWS) {
DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN = Pattern.compile("[\\w\\Q-.\\/:\\E]+");
} else {
// No restrictions
DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN = null;
}
}
/**
* The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if
* cgiPathPrefix is null)
*/
private String cgiPathPrefix = null;
/** the executable to use with the script */
private String cgiExecutable = "perl";
/** additional arguments for the executable */
private List<String> cgiExecutableArgs = null;
/** the encoding to use for parameters */
private String parameterEncoding = System.getProperty("file.encoding", "UTF-8");
/* The HTTP methods this Servlet will pass to the CGI script */
private Set<String> cgiMethods = new HashSet<>();
private boolean cgiMethodsAll = false;
/**
* The time (in milliseconds) to wait for the reading of stderr to complete before terminating the CGI process.
*/
private long stderrTimeout = 2000;
/**
* The regular expression used to select HTTP headers to be passed to the CGI process as environment variables. The
* name of the environment variable will be the name of the HTTP header converter to upper case, prefixed with
* <code>HTTP_</code> and with all <code>-</code> characters converted to <code>_</code>.
*/
private Pattern envHttpHeadersPattern =
Pattern.compile("ACCEPT[-0-9A-Z]*|CACHE-CONTROL|COOKIE|HOST|IF-[-0-9A-Z]*|REFERER|USER-AGENT");
/** object used to ensure multiple threads don't try to expand same file */
private static final Object expandFileLock = new Object();
/** the shell environment variables to be passed to the CGI script */
private final Map<String,String> shellEnv = new HashMap<>();
/**
* Enable creation of script command line arguments from query-string. See
* https://tools.ietf.org/html/rfc3875#section-4.4 4.4. The Script Command Line
*/
private boolean enableCmdLineArguments = false;
/**
* Limits the encoded form of individual command line arguments. By default values are limited to those allowed by
* the RFC. See https://tools.ietf.org/html/rfc3875#section-4.4 Uses \Q...\E to avoid individual quoting.
*/
private Pattern cmdLineArgumentsEncodedPattern = Pattern.compile("[\\w\\Q%;/?:@&,$-.!~*'()\\E]+");
/**
* Limits the decoded form of individual command line arguments. Default varies by platform.
*/
private Pattern cmdLineArgumentsDecodedPattern = DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN;
/**
* Sets instance variables.
* <P>
* Modified from Craig R. McClanahan's InvokerServlet
* </P>
*
* @param config a <code>ServletConfig</code> object containing the servlet's configuration and initialization
* parameters
*
* @exception ServletException if an exception has occurred that interferes with the servlet's normal operation
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// Set our properties from the initialization parameters
cgiPathPrefix = getServletConfig().getInitParameter("cgiPathPrefix");
boolean passShellEnvironment =
Boolean.parseBoolean(getServletConfig().getInitParameter("passShellEnvironment"));
if (passShellEnvironment) {
shellEnv.putAll(System.getenv());
}
Enumeration<String> e = config.getInitParameterNames();
while (e.hasMoreElements()) {
String initParamName = e.nextElement();
if (initParamName.startsWith("environment-variable-")) {
if (initParamName.length() == 21) {
throw new ServletException(sm.getString("cgiServlet.emptyEnvVarName"));
}
shellEnv.put(initParamName.substring(21), config.getInitParameter(initParamName));
}
}
if (getServletConfig().getInitParameter("executable") != null) {
cgiExecutable = getServletConfig().getInitParameter("executable");
}
if (getServletConfig().getInitParameter("executable-arg-1") != null) {
List<String> args = new ArrayList<>();
for (int i = 1;; i++) {
String arg = getServletConfig().getInitParameter("executable-arg-" + i);
if (arg == null) {
break;
}
args.add(arg);
}
cgiExecutableArgs = args;
}
if (getServletConfig().getInitParameter("parameterEncoding") != null) {
parameterEncoding = getServletConfig().getInitParameter("parameterEncoding");
}
if (getServletConfig().getInitParameter("stderrTimeout") != null) {
stderrTimeout = Long.parseLong(getServletConfig().getInitParameter("stderrTimeout"));
}
if (getServletConfig().getInitParameter("envHttpHeaders") != null) {
envHttpHeadersPattern = Pattern.compile(getServletConfig().getInitParameter("envHttpHeaders"));
}
if (getServletConfig().getInitParameter("enableCmdLineArguments") != null) {
enableCmdLineArguments = Boolean.parseBoolean(config.getInitParameter("enableCmdLineArguments"));
}
if (getServletConfig().getInitParameter("cgiMethods") != null) {
String paramValue = getServletConfig().getInitParameter("cgiMethods");
paramValue = paramValue.trim();
if ("*".equals(paramValue)) {
cgiMethodsAll = true;
} else {
String[] methods = paramValue.split(",");
for (String method : methods) {
String trimmedMethod = method.trim();
cgiMethods.add(trimmedMethod);
}
}
} else {
cgiMethods.add("GET");
cgiMethods.add("POST");
}
if (getServletConfig().getInitParameter("cmdLineArgumentsEncoded") != null) {
cmdLineArgumentsEncodedPattern =
Pattern.compile(getServletConfig().getInitParameter("cmdLineArgumentsEncoded"));
}
String value = getServletConfig().getInitParameter("cmdLineArgumentsDecoded");
if (ALLOW_ANY_PATTERN.equals(value)) {
// Optimisation for case where anything is allowed
cmdLineArgumentsDecodedPattern = null;
} else if (value != null) {
cmdLineArgumentsDecodedPattern = Pattern.compile(value);
}
}
/**
* Logs important Servlet API and container information.
* <p>
* Based on SnoopAllServlet by Craig R. McClanahan
* </p>
*
* @param req HttpServletRequest object used as source of information
*
* @exception IOException if a write operation exception occurs
*/
private void printServletEnvironment(HttpServletRequest req) throws IOException {
// Document the properties from ServletRequest
log.trace("ServletRequest Properties");
Enumeration<String> attrs = req.getAttributeNames();
while (attrs.hasMoreElements()) {
String attr = attrs.nextElement();
log.trace("Request Attribute: " + attr + ": [ " + req.getAttribute(attr) + "]");
}
log.trace("Character Encoding: [" + req.getCharacterEncoding() + "]");
log.trace("Content Length: [" + req.getContentLengthLong() + "]");
log.trace("Content Type: [" + req.getContentType() + "]");
Enumeration<Locale> locales = req.getLocales();
while (locales.hasMoreElements()) {
Locale locale = locales.nextElement();
log.trace("Locale: [" + locale + "]");
}
Enumeration<String> params = req.getParameterNames();
while (params.hasMoreElements()) {
String param = params.nextElement();
for (String value : req.getParameterValues(param)) {
log.trace("Request Parameter: " + param + ": [" + value + "]");
}
}
log.trace("Protocol: [" + req.getProtocol() + "]");
log.trace("Remote Address: [" + req.getRemoteAddr() + "]");
log.trace("Remote Host: [" + req.getRemoteHost() + "]");
log.trace("Scheme: [" + req.getScheme() + "]");
log.trace("Secure: [" + req.isSecure() + "]");
log.trace("Server Name: [" + req.getServerName() + "]");
log.trace("Server Port: [" + req.getServerPort() + "]");
// Document the properties from HttpServletRequest
log.trace("HttpServletRequest Properties");
log.trace("Auth Type: [" + req.getAuthType() + "]");
log.trace("Context Path: [" + req.getContextPath() + "]");
Cookie cookies[] = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
log.trace("Cookie: " + cookie.getName() + ": [" + cookie.getValue() + "]");
}
}
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
String header = headers.nextElement();
log.trace("HTTP Header: " + header + ": [" + req.getHeader(header) + "]");
}
log.trace("Method: [" + req.getMethod() + "]");
log.trace("Path Info: [" + req.getPathInfo() + "]");
log.trace("Path Translated: [" + req.getPathTranslated() + "]");
log.trace("Query String: [" + req.getQueryString() + "]");
log.trace("Remote User: [" + req.getRemoteUser() + "]");
log.trace("Requested Session ID: [" + req.getRequestedSessionId() + "]");
log.trace("Requested Session ID From Cookie: [" + req.isRequestedSessionIdFromCookie() + "]");
log.trace("Requested Session ID From URL: [" + req.isRequestedSessionIdFromURL() + "]");
log.trace("Requested Session ID Valid: [" + req.isRequestedSessionIdValid() + "]");
log.trace("Request URI: [" + req.getRequestURI() + "]");
log.trace("Servlet Path: [" + req.getServletPath() + "]");
log.trace("User Principal: [" + req.getUserPrincipal() + "]");
// Process the current session (if there is one)
HttpSession session = req.getSession(false);
if (session != null) {
// Document the session properties
log.trace("HttpSession Properties");
log.trace("ID: [" + session.getId() + "]");
log.trace("Creation Time: [" + new Date(session.getCreationTime()) + "]");
log.trace("Last Accessed Time: [" + new Date(session.getLastAccessedTime()) + "]");
log.trace("Max Inactive Interval: [" + session.getMaxInactiveInterval() + "]");
// Document the session attributes
attrs = session.getAttributeNames();
while (attrs.hasMoreElements()) {
String attr = attrs.nextElement();
log.trace("Session Attribute: " + attr + ": [" + session.getAttribute(attr) + "]");
}
}
// Document the servlet configuration properties
log.trace("ServletConfig Properties");
log.trace("Servlet Name: [" + getServletConfig().getServletName() + "]");
// Document the servlet configuration initialization parameters
params = getServletConfig().getInitParameterNames();
while (params.hasMoreElements()) {
String param = params.nextElement();
String value = getServletConfig().getInitParameter(param);
log.trace("Servlet Init Param: " + param + ": [" + value + "]");
}
// Document the servlet context properties
log.trace("ServletContext Properties");
log.trace("Major Version: [" + getServletContext().getMajorVersion() + "]");
log.trace("Minor Version: [" + getServletContext().getMinorVersion() + "]");
log.trace("Real Path for '/': [" + getServletContext().getRealPath("/") + "]");
log.trace("Server Info: [" + getServletContext().getServerInfo() + "]");
// Document the servlet context initialization parameters
log.trace("ServletContext Initialization Parameters");
params = getServletContext().getInitParameterNames();
while (params.hasMoreElements()) {
String param = params.nextElement();
String value = getServletContext().getInitParameter(param);
log.trace("Servlet Context Init Param: " + param + ": [" + value + "]");
}
// Document the servlet context attributes
log.trace("ServletContext Attributes");
attrs = getServletContext().getAttributeNames();
while (attrs.hasMoreElements()) {
String attr = attrs.nextElement();
log.trace("Servlet Context Attribute: " + attr + ": [" + getServletContext().getAttribute(attr) + "]");
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String method = req.getMethod();
if (cgiMethodsAll || cgiMethods.contains(method)) {
doGet(req, res);
} else if (DEFAULT_SUPER_METHODS.contains(method)) {
// If the CGI servlet is explicitly configured to handle one of
// these methods it will be handled in the previous condition
super.service(req, res);
} else {
// Unsupported method
res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
/**
* Provides CGI Gateway service.
*
* @param req HttpServletRequest passed in by servlet container
* @param res HttpServletResponse passed in by servlet container
*
* @exception ServletException if a servlet-specific exception occurs
* @exception IOException if a read/write exception occurs
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
CGIEnvironment cgiEnv = new CGIEnvironment(req, getServletContext());
if (cgiEnv.isValid()) {
CGIRunner cgi = new CGIRunner(cgiEnv.getCommand(), cgiEnv.getEnvironment(), cgiEnv.getWorkingDirectory(),
cgiEnv.getParameters());
if ("POST".equals(req.getMethod())) {
cgi.setInput(req.getInputStream());
}
cgi.setResponse(res);
cgi.run();
} else {
res.sendError(404);
}
if (log.isTraceEnabled()) {
String[] cgiEnvLines = cgiEnv.toString().split(System.lineSeparator());
for (String cgiEnvLine : cgiEnvLines) {
log.trace(cgiEnvLine);
}
printServletEnvironment(req);
}
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// Note: This method will never be called if cgiMethods is "*" so that
// case does nto need to be handled here.
Set<String> allowedMethods = new HashSet<>();
allowedMethods.addAll(cgiMethods);
allowedMethods.addAll(DEFAULT_SUPER_METHODS);
StringBuilder headerValue = new StringBuilder();
for (String method : allowedMethods) {
headerValue.append(method);
headerValue.append(',');
}
// Remove trailing comma
headerValue.deleteCharAt(headerValue.length() - 1);
res.setHeader("allow", headerValue.toString());
}
/*
* Behaviour depends on the status code.
*
* Status < 400 - Calls setStatus. Returns false. CGI servlet will provide the response body.
*
* Status >= 400 - Calls sendError(status), returns true. Standard error page mechanism will provide the response
* body.
*/
private boolean setStatus(HttpServletResponse response, int status) throws IOException {
if (status >= HttpServletResponse.SC_BAD_REQUEST) {
response.sendError(status);
return true;
} else {
response.setStatus(status);
return false;
}
}
/**
* Encapsulates the CGI environment and rules to derive that environment from the servlet container and request
* information.
*/
protected class CGIEnvironment {
/** context of the enclosing servlet */
private ServletContext context = null;
/** context path of enclosing servlet */
private String contextPath = null;
/** servlet URI of the enclosing servlet */
private String servletPath = null;
/** pathInfo for the current request */
private String pathInfo = null;
/** real file system directory of the enclosing servlet's web app */
private String webAppRootDir = null;
/** tempdir for context - used to expand scripts in unexpanded wars */
private File tmpDir = null;
/** derived cgi environment */
private Map<String,String> env = null;
/** cgi command to be invoked */
private String command = null;
/** cgi command's desired working directory */
private final File workingDirectory;
/** cgi command's command line parameters */
private final ArrayList<String> cmdLineParameters = new ArrayList<>();
/** whether or not this object is valid or not */
private final boolean valid;
/**
* Creates a CGIEnvironment and derives the necessary environment, query parameters, working directory, cgi
* command, etc.
*
* @param req HttpServletRequest for information provided by the Servlet API
* @param context ServletContext for information provided by the Servlet API
*
* @throws IOException an IO error occurred
*/
protected CGIEnvironment(HttpServletRequest req, ServletContext context) throws IOException {
setupFromContext(context);
boolean valid = setupFromRequest(req);
if (valid) {
valid = setCGIEnvironment(req);
}
if (valid) {
workingDirectory = new File(command.substring(0, command.lastIndexOf(File.separator)));
} else {
workingDirectory = null;
}
this.valid = valid;
}
/**
* Uses the ServletContext to set some CGI variables
*
* @param context ServletContext for information provided by the Servlet API
*/
protected void setupFromContext(ServletContext context) {
this.context = context;
this.webAppRootDir = context.getRealPath("/");
this.tmpDir = (File) context.getAttribute(ServletContext.TEMPDIR);
}
/**
* Uses the HttpServletRequest to set most CGI variables
*
* @param req HttpServletRequest for information provided by the Servlet API
*
* @return true if the request was parsed without error, false if there was a problem
*
* @throws UnsupportedEncodingException Unknown encoding
*/
protected boolean setupFromRequest(HttpServletRequest req) throws UnsupportedEncodingException {
boolean isIncluded = false;
// Look to see if this request is an include
if (req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
isIncluded = true;
}
if (isIncluded) {
this.contextPath = (String) req.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH);
this.servletPath = (String) req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
this.pathInfo = (String) req.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
} else {
this.contextPath = req.getContextPath();
this.servletPath = req.getServletPath();
this.pathInfo = req.getPathInfo();
}
// If getPathInfo() returns null, must be using extension mapping
// In this case, pathInfo should be same as servletPath
if (this.pathInfo == null) {
this.pathInfo = this.servletPath;
}
// If the request method is GET, POST or HEAD and the query string
// does not contain an unencoded "=" this is an indexed query.
// The parsed query string becomes the command line parameters
// for the cgi command.
if (enableCmdLineArguments && (req.getMethod().equals("GET") || req.getMethod().equals("POST") ||
req.getMethod().equals("HEAD"))) {
String qs;
if (isIncluded) {
qs = (String) req.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
} else {
qs = req.getQueryString();
}
if (qs != null && qs.indexOf('=') == -1) {
StringTokenizer qsTokens = new StringTokenizer(qs, "+");
while (qsTokens.hasMoreTokens()) {
String encodedArgument = qsTokens.nextToken();
if (!cmdLineArgumentsEncodedPattern.matcher(encodedArgument).matches()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("cgiServlet.invalidArgumentEncoded", encodedArgument,
cmdLineArgumentsEncodedPattern.toString()));
}
return false;
}
String decodedArgument = URLDecoder.decode(encodedArgument, parameterEncoding);
if (cmdLineArgumentsDecodedPattern != null &&
!cmdLineArgumentsDecodedPattern.matcher(decodedArgument).matches()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("cgiServlet.invalidArgumentDecoded", decodedArgument,
cmdLineArgumentsDecodedPattern.toString()));
}
return false;
}
cmdLineParameters.add(decodedArgument);
}
}
}
return true;
}
/**
* Resolves core information about the cgi script.
* <p>
* Example URI:
* </p>
*
* <PRE>
* /servlet/cgigateway/dir1/realCGIscript/pathinfo1
* </PRE>
* <ul>
* <LI><b>path</b> = $CATALINA_HOME/mywebapp/dir1/realCGIscript
* <LI><b>scriptName</b> = /servlet/cgigateway/dir1/realCGIscript
* <LI><b>cgiName</b> = /dir1/realCGIscript
* <LI><b>name</b> = realCGIscript
* </ul>
* <p>
* CGI search algorithm: search the real path below <my-webapp-root> and find the first non-directory in
* the getPathTranslated("/"), reading/searching from left-to-right.
* </p>
* <p>
* The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if
* cgiPathPrefix is null).
* </p>
* <p>
* cgiPathPrefix is defined by setting this servlet's cgiPathPrefix init parameter
* </p>
*
* @param pathInfo String from HttpServletRequest.getPathInfo()
* @param webAppRootDir String from context.getRealPath("/")
* @param contextPath String as from HttpServletRequest.getContextPath()
* @param servletPath String as from HttpServletRequest.getServletPath()
* @param cgiPathPrefix subdirectory of webAppRootDir below which the web app's CGIs may be stored; can be null.
* The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or
* webAppRootDir alone if cgiPathPrefix is null). cgiPathPrefix is defined by setting
* the servlet's cgiPathPrefix init parameter.
*
* @return
* <ul>
* <li><code>path</code> - full file-system path to valid cgi script, or null if no cgi was found
* <li><code>scriptName</code> - CGI variable SCRIPT_NAME; the full URL path to valid cgi script or
* null if no cgi was found
* <li><code>cgiName</code> - servlet pathInfo fragment corresponding to the cgi script itself, or
* null if not found
* <li><code>name</code> - simple name (no directories) of the cgi script, or null if no cgi was
* found
* </ul>
*/
protected String[] findCGI(String pathInfo, String webAppRootDir, String contextPath, String servletPath,
String cgiPathPrefix) {
String path = null;
String name = null;
String scriptname = null;
if (webAppRootDir.lastIndexOf(File.separator) == (webAppRootDir.length() - 1)) {
// strip the trailing "/" from the webAppRootDir
webAppRootDir = webAppRootDir.substring(0, (webAppRootDir.length() - 1));
}
if (cgiPathPrefix != null) {
webAppRootDir = webAppRootDir + File.separator + cgiPathPrefix;
}
if (log.isTraceEnabled()) {
log.trace(sm.getString("cgiServlet.find.path", pathInfo, webAppRootDir));
}
File currentLocation = new File(webAppRootDir);
StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/");
if (log.isTraceEnabled()) {
log.trace(sm.getString("cgiServlet.find.location", currentLocation.getAbsolutePath()));
}
StringBuilder cginameBuilder = new StringBuilder();
while (!currentLocation.isFile() && dirWalker.hasMoreElements()) {
String nextElement = (String) dirWalker.nextElement();
currentLocation = new File(currentLocation, nextElement);
cginameBuilder.append('/').append(nextElement);
if (log.isTraceEnabled()) {
log.trace(sm.getString("cgiServlet.find.location", currentLocation.getAbsolutePath()));
}
}
String cginame = cginameBuilder.toString();
if (!currentLocation.isFile()) {
return new String[] { null, null, null, null };
}
path = currentLocation.getAbsolutePath();
name = currentLocation.getName();
if (servletPath.startsWith(cginame)) {
scriptname = contextPath + cginame;
} else {
scriptname = contextPath + servletPath + cginame;
}
if (log.isTraceEnabled()) {
log.trace(sm.getString("cgiServlet.find.found", name, path, scriptname, cginame));
}
return new String[] { path, scriptname, cginame, name };
}
/**
* Constructs the CGI environment to be supplied to the invoked CGI script; relies heavily on Servlet API
* methods and findCGI
*
* @param req request associated with the CGI Invocation
*
* @return true if environment was set OK, false if there was a problem and no environment was set
*
* @throws IOException an IO error occurred
*/
protected boolean setCGIEnvironment(HttpServletRequest req) throws IOException {
/*
* This method is slightly ugly; c'est la vie.
* "You cannot stop [ugliness], you can only hope to contain [it]" (apologies to Marv Albert regarding MJ)
*/
// Add the shell environment variables (if any)
Map<String,String> envp = new HashMap<>(shellEnv);
// Add the CGI environment variables
String sPathInfoOrig = null;
String sPathInfoCGI = null;
String sPathTranslatedCGI = null;
String sCGIFullPath = null;
String sCGIScriptName = null;
String sCGIFullName = null;
String sCGIName = null;
String[] sCGINames;
sPathInfoOrig = this.pathInfo;
sPathInfoOrig = sPathInfoOrig == null ? "" : sPathInfoOrig;
if (webAppRootDir == null) {
// The app has not been deployed in exploded form
webAppRootDir = tmpDir.toString();
expandCGIScript();
}
sCGINames = findCGI(sPathInfoOrig, webAppRootDir, contextPath, servletPath, cgiPathPrefix);
sCGIFullPath = sCGINames[0];
sCGIScriptName = sCGINames[1];
sCGIFullName = sCGINames[2];
sCGIName = sCGINames[3];
if (sCGIFullPath == null || sCGIScriptName == null || sCGIFullName == null || sCGIName == null) {
return false;
}
envp.put("SERVER_SOFTWARE", "TOMCAT");
envp.put("SERVER_NAME", nullsToBlanks(req.getServerName()));
envp.put("GATEWAY_INTERFACE", "CGI/1.1");
envp.put("SERVER_PROTOCOL", nullsToBlanks(req.getProtocol()));
int port = req.getServerPort();
Integer iPort = (port == 0 ? Integer.valueOf(-1) : Integer.valueOf(port));
envp.put("SERVER_PORT", iPort.toString());
envp.put("REQUEST_METHOD", nullsToBlanks(req.getMethod()));
envp.put("REQUEST_URI", nullsToBlanks(req.getRequestURI()));
/*-
* PATH_INFO should be determined by using sCGIFullName:
* 1) Let sCGIFullName not end in a "/" (see method findCGI)
* 2) Let sCGIFullName equal the pathInfo fragment which
* corresponds to the actual cgi script.
* 3) Thus, PATH_INFO = request.getPathInfo().substring(
* sCGIFullName.length())
*
* (see method findCGI, where the real work is done)
*
*/
if (pathInfo == null || (pathInfo.substring(sCGIFullName.length()).length() <= 0)) {
sPathInfoCGI = "";
} else {
sPathInfoCGI = pathInfo.substring(sCGIFullName.length());
}
envp.put("PATH_INFO", sPathInfoCGI);
/*-
* PATH_TRANSLATED must be determined after PATH_INFO (and the
* implied real cgi-script) has been taken into account.
*
* The following example demonstrates:
*
* servlet info = /servlet/cgigw/dir1/dir2/cgi1/trans1/trans2
* cgifullpath = /servlet/cgigw/dir1/dir2/cgi1
* path_info = /trans1/trans2
* webAppRootDir = servletContext.getRealPath("/")
*
* path_translated = servletContext.getRealPath("/trans1/trans2")
*
* That is, PATH_TRANSLATED = webAppRootDir + sPathInfoCGI
* (unless sPathInfoCGI is null or blank, then the CGI
* specification dictates that the PATH_TRANSLATED metavariable
* SHOULD NOT be defined.
*
*/
if (!sPathInfoCGI.isEmpty()) {
sPathTranslatedCGI = context.getRealPath(sPathInfoCGI);
}
if (sPathTranslatedCGI == null || "".equals(sPathTranslatedCGI)) {
// NOOP
} else {
envp.put("PATH_TRANSLATED", nullsToBlanks(sPathTranslatedCGI));
}
envp.put("SCRIPT_NAME", nullsToBlanks(sCGIScriptName));
envp.put("QUERY_STRING", nullsToBlanks(req.getQueryString()));
envp.put("REMOTE_HOST", nullsToBlanks(req.getRemoteHost()));
envp.put("REMOTE_ADDR", nullsToBlanks(req.getRemoteAddr()));
envp.put("AUTH_TYPE", nullsToBlanks(req.getAuthType()));
envp.put("REMOTE_USER", nullsToBlanks(req.getRemoteUser()));
envp.put("REMOTE_IDENT", ""); // not necessary for full compliance
envp.put("CONTENT_TYPE", nullsToBlanks(req.getContentType()));
/*
* Note CGI spec says CONTENT_LENGTH must be NULL ("") or undefined if there is no content, so we cannot put
* 0 or -1 in as per the Servlet API spec.
*/
long contentLength = req.getContentLengthLong();
String sContentLength = (contentLength <= 0 ? "" : Long.toString(contentLength));
envp.put("CONTENT_LENGTH", sContentLength);
Enumeration<String> headers = req.getHeaderNames();
String header = null;
while (headers.hasMoreElements()) {
header = null;
header = headers.nextElement().toUpperCase(Locale.ENGLISH);
// REMIND: rewrite multiple headers as if received as single
// REMIND: change character set
// REMIND: I forgot what the previous REMIND means
if (envHttpHeadersPattern.matcher(header).matches()) {
envp.put("HTTP_" + header.replace('-', '_'), req.getHeader(header));
}
}
File fCGIFullPath = new File(sCGIFullPath);
command = fCGIFullPath.getCanonicalPath();
envp.put("X_TOMCAT_SCRIPT_PATH", command); // for kicks
envp.put("SCRIPT_FILENAME", command); // for PHP
this.env = envp;
return true;
}
/**
* Extracts requested resource from web app archive to context work directory to enable CGI script to be
* executed.
*/
protected void expandCGIScript() {
StringBuilder srcPath = new StringBuilder();
StringBuilder destPath = new StringBuilder();
InputStream is = null;
// paths depend on mapping
if (cgiPathPrefix == null) {
srcPath.append(pathInfo);
is = context.getResourceAsStream(srcPath.toString());
destPath.append(tmpDir);
destPath.append(pathInfo);
} else {
// essentially same search algorithm as findCGI()
srcPath.append(cgiPathPrefix);
StringTokenizer pathWalker = new StringTokenizer(pathInfo, "/");
// start with first element
while (pathWalker.hasMoreElements() && (is == null)) {
srcPath.append('/');
srcPath.append(pathWalker.nextElement());
is = context.getResourceAsStream(srcPath.toString());
}
destPath.append(tmpDir);
destPath.append('/');
destPath.append(srcPath);
}
if (is == null) {
// didn't find anything, give up now
log.warn(sm.getString("cgiServlet.expandNotFound", srcPath));
return;
}
try {
File f = new File(destPath.toString());
if (f.exists()) {
// Don't need to expand if it already exists
return;
}
// create directories
File dir = f.getParentFile();
if (!dir.mkdirs() && !dir.isDirectory()) {
log.warn(sm.getString("cgiServlet.expandCreateDirFail", dir.getAbsolutePath()));
return;
}
try {
synchronized (expandFileLock) {
// make sure file doesn't exist
if (f.exists()) {
return;
}
// create file
if (!f.createNewFile()) {
return;
}
Files.copy(is, f.toPath());
if (log.isDebugEnabled()) {
log.debug(sm.getString("cgiServlet.expandOk", srcPath, destPath));
}
}
} catch (IOException ioe) {
log.warn(sm.getString("cgiServlet.expandFail", srcPath, destPath), ioe);
// delete in case file is corrupted
if (f.exists()) {
if (!f.delete()) {
log.warn(sm.getString("cgiServlet.expandDeleteFail", f.getAbsolutePath()));
}
}
}
} finally {
try {
is.close();
} catch (IOException e) {
log.warn(sm.getString("cgiServlet.expandCloseFail", srcPath), e);
}
}
}
/**
* Returns important CGI environment information in a multi-line text format.
*
* @return CGI environment info
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CGIEnvironment Info:");
sb.append(System.lineSeparator());
if (isValid()) {
sb.append("Validity: [true]");
sb.append(System.lineSeparator());
sb.append("Environment values:");
sb.append(System.lineSeparator());
for (Entry<String,String> entry : env.entrySet()) {
sb.append(" ");
sb.append(entry.getKey());
sb.append(": [");
sb.append(blanksToString(entry.getValue(), "will be set to blank"));
sb.append(']');
sb.append(System.lineSeparator());
}
sb.append("Derived Command :[");
sb.append(nullsToBlanks(command));
sb.append(']');
sb.append(System.lineSeparator());
sb.append("Working Directory: [");
if (workingDirectory != null) {
sb.append(workingDirectory.toString());
}
sb.append(']');
sb.append(System.lineSeparator());
sb.append("Command Line Params:");
sb.append(System.lineSeparator());
for (String param : cmdLineParameters) {
sb.append(" [");
sb.append(param);
sb.append(']');
sb.append(System.lineSeparator());
}
} else {
sb.append("Validity: [false]");
sb.append(System.lineSeparator());
sb.append("CGI script not found or not specified.");
sb.append(System.lineSeparator());
sb.append("Check the HttpServletRequest pathInfo property to see if it is what ");
sb.append(System.lineSeparator());
sb.append("you meant it to be. You must specify an existent and executable file ");
sb.append(System.lineSeparator());
sb.append("as part of the path-info.");
sb.append(System.lineSeparator());
}
return sb.toString();
}
/**
* Gets derived command string
*
* @return command string
*/
protected String getCommand() {
return command;
}
/**
* Gets derived CGI working directory
*
* @return working directory
*/
protected File getWorkingDirectory() {
return workingDirectory;
}
/**
* Gets derived CGI environment
*
* @return CGI environment
*/
protected Map<String,String> getEnvironment() {
return env;
}
/**
* Gets derived CGI query parameters
*
* @return CGI query parameters
*/
protected ArrayList<String> getParameters() {
return cmdLineParameters;
}
/**
* Gets validity status
*
* @return true if this environment is valid, false otherwise
*/
protected boolean isValid() {
return valid;
}
/**
* Converts null strings to blank strings ("")
*
* @param s string to be converted if necessary
*
* @return a non-null string, either the original or the empty string ("") if the original was <code>null</code>
*/
protected String nullsToBlanks(String s) {
return nullsToString(s, "");
}
/**
* Converts null strings to another string
*
* @param couldBeNull string to be converted if necessary
* @param subForNulls string to return instead of a null string
*
* @return a non-null string, either the original or the substitute string if the original was <code>null</code>
*/
protected String nullsToString(String couldBeNull, String subForNulls) {
return (couldBeNull == null ? subForNulls : couldBeNull);
}
/**
* Converts blank strings to another string
*
* @param couldBeBlank string to be converted if necessary
* @param subForBlanks string to return instead of a blank string
*
* @return a non-null string, either the original or the substitute string if the original was <code>null</code>
* or empty ("")
*/
protected String blanksToString(String couldBeBlank, String subForBlanks) {
return (couldBeBlank == null || couldBeBlank.isEmpty()) ? subForBlanks : couldBeBlank;
}
} // class CGIEnvironment
/**
* Encapsulates the knowledge of how to run a CGI script, given the script's desired environment and (optionally)
* input/output streams
* <p>
* Exposes a <code>run</code> method used to actually invoke the CGI.
* </p>
* <p>
* The CGI environment and settings are derived from the information passed to the constructor.
* </p>
* <p>
* The input and output streams can be set by the <code>setInput</code> and <code>setResponse</code> methods,
* respectively.
* </p>
*/
protected class CGIRunner {
/** script/command to be executed */
private final String command;
/** environment used when invoking the cgi script */
private final Map<String,String> env;
/** working directory used when invoking the cgi script */
private final File wd;
/** command line parameters to be passed to the invoked script */
private final ArrayList<String> params;
/** stdin to be passed to cgi script */
private InputStream stdin = null;
/** response object used to set headers & get output stream */
private HttpServletResponse response = null;
/** boolean tracking whether this object has enough info to run() */
private boolean readyToRun = false;
/**
* Creates a CGIRunner and initializes its environment, working directory, and query parameters. <BR>
* Input/output streams (optional) are set using the <code>setInput</code> and <code>setResponse</code> methods,
* respectively.
*
* @param command string full path to command to be executed
* @param env Map with the desired script environment
* @param wd File with the script's desired working directory
* @param params ArrayList with the script's query command line parameters as strings
*/
protected CGIRunner(String command, Map<String,String> env, File wd, ArrayList<String> params) {
this.command = command;
this.env = env;
this.wd = wd;
this.params = params;
updateReadyStatus();
}
/**
* Checks and sets ready status
*/
protected void updateReadyStatus() {
if (command != null && env != null && wd != null && params != null && response != null) {
readyToRun = true;
} else {
readyToRun = false;
}
}
/**
* Gets ready status
*
* @return false if not ready (<code>run</code> will throw an exception), true if ready
*/
protected boolean isReady() {
return readyToRun;
}
/**
* Sets HttpServletResponse object used to set headers and send output to
*
* @param response HttpServletResponse to be used
*/
protected void setResponse(HttpServletResponse response) {
this.response = response;
updateReadyStatus();
}
/**
* Sets standard input to be passed on to the invoked cgi script
*
* @param stdin InputStream to be used
*/
protected void setInput(InputStream stdin) {
this.stdin = stdin;
updateReadyStatus();
}
/**
* Converts a Map to a String array by converting each key/value pair in the Map to a String in the form
* "key=value" (key + "=" + map.get(key).toString())
*
* @param map Map to convert
*
* @return converted string array
*
* @exception NullPointerException if a hash key has a null value
*/
protected String[] mapToStringArray(Map<String,?> map) throws NullPointerException {
List<String> list = new ArrayList<>(map.size());
for (Entry<String,?> entry : map.entrySet()) {
list.add(entry.getKey() + "=" + entry.getValue().toString());
}
return list.toArray(new String[0]);
}
/**
* Executes a CGI script with the desired environment, current working directory, and input/output streams
* <p>
* This implements the following CGI specification recommendations:
* </p>
* <UL>
* <LI>Servers SHOULD provide the "<code>query</code>" component of the script-URI as command-line arguments to
* scripts if it does not contain any unencoded "=" characters and the command-line arguments can be generated
* in an unambiguous manner.
* <LI>Servers SHOULD set the AUTH_TYPE metavariable to the value of the "<code>auth-scheme</code>" token of the
* "<code>Authorization</code>" if it was supplied as part of the request header. See
* <code>getCGIEnvironment</code> method.
* <LI>Where applicable, servers SHOULD set the current working directory to the directory in which the script
* is located before invoking it.
* <LI>Server implementations SHOULD define their behavior for the following cases:
* <ul>
* <LI><u>Allowed characters in pathInfo</u>: This implementation does not allow ASCII NUL nor any character
* which cannot be URL-encoded according to internet standards;
* <LI><u>Allowed characters in path segments</u>: This implementation does not allow non-terminal NULL segments
* in the the path -- IOExceptions may be thrown;
* <LI><u>"<code>.</code>" and "<code>..</code>" path segments</u>: This implementation does not allow
* "<code>.</code>" and "<code>..</code>" in the the path, and such characters will result in an IOException
* being thrown (this should never happen since Tomcat normalises the requestURI before determining the
* contextPath, servletPath and pathInfo);
* <LI><u>Implementation limitations</u>: This implementation does not impose any limitations except as
* documented above. This implementation may be limited by the servlet container used to house this
* implementation. In particular, all the primary CGI variable values are derived either directly or indirectly
* from the container's implementation of the Servlet API methods.
* </ul>
* </UL>
*
* @exception IOException if problems during reading/writing occur
*
* @see java.lang.Runtime#exec(String command, String[] envp, File dir)
*/
protected void run() throws IOException {
/*
* REMIND: this method feels too big; should it be re-written?
*/
if (!isReady()) {
throw new IOException(sm.getString("cgiServlet.notReady"));
}
if (log.isTraceEnabled()) {
log.trace("envp: [" + env + "], command: [" + command + "]");
}
if ((command.contains(File.separator + "." + File.separator)) ||
(command.contains(File.separator + "..")) || (command.contains(".." + File.separator))) {
throw new IOException(sm.getString("cgiServlet.invalidCommand", command));
}
/*
* original content/structure of this section taken from
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4216884 with major modifications by Martin Dengler
*/
Runtime rt = null;
BufferedReader cgiHeaderReader = null;
InputStream cgiOutput = null;
BufferedReader commandsStdErr = null;
Thread errReaderThread = null;
BufferedOutputStream commandsStdIn = null;
Process proc = null;
int bufRead = -1;
List<String> cmdAndArgs = new ArrayList<>();
if (cgiExecutable.length() != 0) {
cmdAndArgs.add(cgiExecutable);
}
if (cgiExecutableArgs != null) {
cmdAndArgs.addAll(cgiExecutableArgs);
}
cmdAndArgs.add(command);
cmdAndArgs.addAll(params);
try {
rt = Runtime.getRuntime();
proc = rt.exec(cmdAndArgs.toArray(new String[0]), mapToStringArray(env), wd);
String sContentLength = env.get("CONTENT_LENGTH");
if (!"".equals(sContentLength)) {
commandsStdIn = new BufferedOutputStream(proc.getOutputStream());
IOTools.flow(stdin, commandsStdIn);
commandsStdIn.flush();
commandsStdIn.close();
}
/*
* we want to wait for the process to exit, Process.waitFor() is useless in our situation; see
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4223650
*/
boolean isRunning = true;
commandsStdErr = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
final BufferedReader stdErrRdr = commandsStdErr;
errReaderThread = new Thread(() -> sendToLog(stdErrRdr));
errReaderThread.start();
InputStream cgiHeaderStream = new HTTPHeaderInputStream(proc.getInputStream());
cgiHeaderReader = new BufferedReader(new InputStreamReader(cgiHeaderStream));
// Need to be careful here. If sendError() is called the
// response body should be provided by the standard error page
// process. But, if the output of the CGI process isn't read
// then that process can hang.
boolean skipBody = false;
while (isRunning) {
try {
// set headers
String line = null;
while (((line = cgiHeaderReader.readLine()) != null) && !line.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("addHeader(\"" + line + "\")");
}
if (line.startsWith("HTTP")) {
skipBody = setStatus(response, getSCFromHttpStatusLine(line));
} else if (line.indexOf(':') >= 0) {
String header = line.substring(0, line.indexOf(':')).trim();
String value = line.substring(line.indexOf(':') + 1).trim();
if (header.equalsIgnoreCase("status")) {
skipBody = setStatus(response, getSCFromCGIStatusHeader(value));
} else {
response.addHeader(header, value);
}
} else {
log.info(sm.getString("cgiServlet.runBadHeader", line));
}
}
// write output
byte[] bBuf = new byte[2048];
OutputStream out = response.getOutputStream();
cgiOutput = proc.getInputStream();
try {
while (!skipBody && (bufRead = cgiOutput.read(bBuf)) != -1) {
if (log.isTraceEnabled()) {
log.trace("output " + bufRead + " bytes of data");
}
out.write(bBuf, 0, bufRead);
}
} finally {
// Attempt to consume any leftover byte if something bad happens,
// such as a socket disconnect on the servlet side; otherwise, the
// external process could hang
if (bufRead != -1) {
while ((bufRead = cgiOutput.read(bBuf)) != -1) {
// NOOP - just read the data
}
}
}
proc.exitValue(); // Throws exception if alive
isRunning = false;
} catch (IllegalThreadStateException e) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
// Ignore
}
}
} // replacement for Process.waitFor()
} catch (IOException e) {
log.warn(sm.getString("cgiServlet.runFail"), e);
throw e;
} finally {
// Close the header reader
if (cgiHeaderReader != null) {
try {
cgiHeaderReader.close();
} catch (IOException ioe) {
log.warn(sm.getString("cgiServlet.runHeaderReaderFail"), ioe);
}
}
// Close the output stream if used
if (cgiOutput != null) {
try {
cgiOutput.close();
} catch (IOException ioe) {
log.warn(sm.getString("cgiServlet.runOutputStreamFail"), ioe);
}
}
// Make sure the error stream reader has finished
if (errReaderThread != null) {
try {
errReaderThread.join(stderrTimeout);
} catch (InterruptedException e) {
log.warn(sm.getString("cgiServlet.runReaderInterrupt"));
}
}
if (proc != null) {
proc.destroy();
proc = null;
}
}
}
/**
* Parses the Status-Line and extracts the status code.
*
* @param line The HTTP Status-Line (RFC2616, section 6.1)
*
* @return The extracted status code or the code representing an internal error if a valid status code cannot be
* extracted.
*/
private int getSCFromHttpStatusLine(String line) {
int statusStart = line.indexOf(' ') + 1;
if (statusStart < 1 || line.length() < statusStart + 3) {
// Not a valid HTTP Status-Line
log.warn(sm.getString("cgiServlet.runInvalidStatus", line));
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
String status = line.substring(statusStart, statusStart + 3);
int statusCode;
try {
statusCode = Integer.parseInt(status);
} catch (NumberFormatException nfe) {
// Not a valid status code
log.warn(sm.getString("cgiServlet.runInvalidStatus", status));
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
return statusCode;
}
/**
* Parses the CGI Status Header value and extracts the status code.
*
* @param value The CGI Status value of the form <code>
* digit digit digit SP reason-phrase</code>
*
* @return The extracted status code or the code representing an internal error if a valid status code cannot be
* extracted.
*/
private int getSCFromCGIStatusHeader(String value) {
if (value.length() < 3) {
// Not a valid status value
log.warn(sm.getString("cgiServlet.runInvalidStatus", value));
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
String status = value.substring(0, 3);
int statusCode;
try {
statusCode = Integer.parseInt(status);
} catch (NumberFormatException nfe) {
// Not a valid status code
log.warn(sm.getString("cgiServlet.runInvalidStatus", status));
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
return statusCode;
}
private void sendToLog(BufferedReader rdr) {
String line = null;
int lineCount = 0;
try {
while ((line = rdr.readLine()) != null) {
log.warn(sm.getString("cgiServlet.runStdErr", line));
lineCount++;
}
} catch (IOException e) {
log.warn(sm.getString("cgiServlet.runStdErrFail"), e);
} finally {
try {
rdr.close();
} catch (IOException e) {
log.warn(sm.getString("cgiServlet.runStdErrFail"), e);
}
}
if (lineCount > 0) {
log.warn(sm.getString("cgiServlet.runStdErrCount", Integer.valueOf(lineCount)));
}
}
} // class CGIRunner
/**
* This is an input stream specifically for reading HTTP headers. It reads up to and including the two blank lines
* terminating the headers. It allows the content to be read using bytes or characters as appropriate.
*/
protected static class HTTPHeaderInputStream extends InputStream {
private static final int STATE_CHARACTER = 0;
private static final int STATE_FIRST_CR = 1;
private static final int STATE_FIRST_LF = 2;
private static final int STATE_SECOND_CR = 3;
private static final int STATE_HEADER_END = 4;
private final InputStream input;
private int state;
HTTPHeaderInputStream(InputStream theInput) {
input = theInput;
state = STATE_CHARACTER;
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() throws IOException {
if (state == STATE_HEADER_END) {
return -1;
}
int i = input.read();
/*
* Update the state
* State machine looks like this
* @formatter:off
*
* -------->--------
* | (CR) |
* | |
* CR1--->--- |
* | | |
* ^(CR) |(LF) |
* | | |
* CHAR--->--LF1--->--EOH
* (LF) | (LF) |
* |(CR) ^(LF)
* | |
* (CR2)-->---
*
* @formatter:on
*/
if (i == 10) {
// LF
switch (state) {
case STATE_CHARACTER:
state = STATE_FIRST_LF;
break;
case STATE_FIRST_CR:
state = STATE_FIRST_LF;
break;
case STATE_FIRST_LF:
case STATE_SECOND_CR:
state = STATE_HEADER_END;
break;
}
} else if (i == 13) {
// CR
switch (state) {
case STATE_CHARACTER:
state = STATE_FIRST_CR;
break;
case STATE_FIRST_CR:
state = STATE_HEADER_END;
break;
case STATE_FIRST_LF:
state = STATE_SECOND_CR;
break;
}
} else {
state = STATE_CHARACTER;
}
return i;
}
} // class HTTPHeaderInputStream
} // class CGIServlet