/*
* 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 > & lt ; web - app & gt ; / WEB - INF / web . xml < / code > ) to :
* < / p >
* < p >
* < code >
* & lt ; web - app & gt ; / 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 >
* & lt ; web - app - root & gt ; / 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 > & lt ; webapp & gt ; / 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 =
1 L;
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 & lt ; my - webapp - root & gt ; 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.isDebugEnabled()) {
log.debug(sm.getString("cgiServlet.find.path" , pathInfo, webAppRootDir));
}
File currentLocation = new File(webAppRootDir);
StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/" );
if (log.isDebugEnabled()) {
log.debug(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.isDebugEnabled()) {
log.debug(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.isDebugEnabled()) {
log.debug(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(this .getClass().getName() + ": not ready to run." );
}
if (log.isDebugEnabled()) {
log.debug("envp: [" + env + "], command: [" + command + "]" );
}
if ((command.contains(File.separator + "." + File.separator)) ||
(command.contains(File.separator + ".." )) || (command.contains(".." + File.separator))) {
throw new IOException(this .getClass().getName() + "Illegal Character in CGI command path " +
"('.' or '..') detected. Not running CGI [" + 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
Messung V0.5 in Prozent C=93 H=92 G=92
¤ Dauer der Verarbeitung: 0.52 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland