/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.web.php;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.Globals;
import org.apache.catalina.util.IOTools;
import org.jboss.logging.Logger;
import org.jboss.logging.Logger;
/**
* Encapsulates the Script Environment and rules to derive
* that environment from the servlet container and request information.
*
* @author Mladen Turk
* @version $Revision: 1.1 $, $Date: 2012/08/17 14:43:39 $
* @since 1.0
*/
public class ScriptEnvironment {
private static Logger log = Logger.getLogger(ScriptEnvironment.class);
/**
* The Request attribute key for the client certificate chain.
*/
private static final String CERTIFICATE_KEY = "javax.servlet.request.X509Certificate";
private static final String CIPHER_SUITE = "javax.servlet.request.cipher_suite";
private static final String SSL_SESSION = "javax.servlet.request.ssl_session";
private static final String KEY_SIZE = "javax.servlet.request.key_size";
/** context of the enclosing servlet */
private ServletContext context = null;
/** full path to the script file */
private String scriptFullPath = 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 tempDir = null;
/** derived script environment */
private Hashtable env = null;
/** script's desired working directory */
private File workingDirectory = null;
/** script's desired working directory */
private File scriptFile = null;
/** query parameters */
private ArrayList queryParameters = new ArrayList();
/** whether or not this object is valid or not */
private boolean valid = false;
/** object used to ensure multiple threads don't try to expand same file */
private static Object expandFileLock = new Object();
/**
* The Script search path will start at
* webAppRootDir + File.separator + scriptPathPrefix
* (or webAppRootDir alone if scriptPathPrefix is
* null)
*/
private String scriptPathPrefix = null;
/**
* Resolves core information about the php script.
*
*
* Example URI:
*
/servlet/scriptGateway/dir1/realScript/pathinfo1
*
* path
= $CATALINA_HOME/mywebapp/dir1/realScript
* scriptName
= /servlet/scriptGateway/dir1/realScript
* fullName
= /dir1/realScript
* name
= realScript
*
*
*
* Script 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.
*
*
* The Script search path will start at
* webAppRootDir + File.separator + scriptPathPrefix
* (or webAppRootDir alone if scriptPathPrefix is
* null).
*
*
* scriptPathPrefix is defined by setting
* this servlet's scriptPathPrefix init parameter
*
*
*
* @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 scriptPathPrefix Subdirectory of webAppRootDir below which
* the web app's Scripts may be stored; can be null.
* The Script search path will start at
* webAppRootDir + File.separator + scriptPathPrefix
* (or webAppRootDir alone if scriptPathPrefix is
* null). scriptPathPrefix is defined by setting
* the servlet's scriptPathPrefix init parameter.
*
*
* @return
*
* -
*
path
- full file-system path to valid script file,
* or null if no script file was found
* -
*
scriptName
-
* Script variable SCRIPT_NAME; the full URL path
* to valid script file or null if no script was
* found
* -
*
fullName
- servlet pathInfo fragment corresponding to
* the script itself, or null if not found
* -
*
name
- simple name (no directories) of the
* script, or null if no script was found
*
*
*/
protected String[] findScript(String pathInfo, String webAppRootDir,
String contextPath, String servletPath,
String scriptPathPrefix)
{
String path = null;
String name = null;
String scriptName = null;
String fullName = null;
if ((webAppRootDir != null)
&& (webAppRootDir.lastIndexOf(File.separator) ==
(webAppRootDir.length() - 1))) {
//strip the trailing "/" from the webAppRootDir
webAppRootDir =
webAppRootDir.substring(0, (webAppRootDir.length() - 1));
}
if (scriptPathPrefix != null) {
webAppRootDir = webAppRootDir + File.separator
+ scriptPathPrefix;
}
File currentLocation = new File(webAppRootDir);
StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/");
while (!currentLocation.isFile() && dirWalker.hasMoreElements()) {
currentLocation = new File(currentLocation,
(String)dirWalker.nextElement());
}
if (!currentLocation.isFile()) {
return new String[] { null, null, null, null };
}
else {
path = currentLocation.getAbsolutePath();
name = currentLocation.getName();
fullName =
currentLocation.getParent().substring(webAppRootDir.length())
+ File.separator + name;
// NOTE: Original CGI messes the Win path.
fullName = fullName.replace(File.separatorChar, '/');
if (contextPath != null && ! "".equals(contextPath) && ! "/".equals(contextPath)) {
scriptName = contextPath + fullName;
}
else {
// NOTE: set scriptName to fullName
scriptName = fullName;
}
}
return new String[] { path, scriptName, fullName, name };
}
/**
* Extracts requested resource from web app archive to context work
* directory to enable script to be executed.
*/
protected void expandScript()
{
StringBuffer srcPath = new StringBuffer();
StringBuffer dstPath = new StringBuffer();
InputStream is = null;
// paths depend on mapping
if (scriptPathPrefix == null) {
srcPath.append(pathInfo);
is = context.getResourceAsStream(srcPath.toString());
dstPath.append(tempDir);
dstPath.append(pathInfo);
}
else {
// essentially same search algorithm as findScript()
srcPath.append(scriptPathPrefix);
StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/");
// start with first element
while (dirWalker.hasMoreElements() && (is == null)) {
srcPath.append("/");
srcPath.append(dirWalker.nextElement());
is = context.getResourceAsStream(srcPath.toString());
}
dstPath.append(tempDir);
dstPath.append("/");
dstPath.append(srcPath);
}
if (is == null) {
// didn't find anything, give up now
return;
}
File f = new File(dstPath.toString());
if (f.exists()) {
// Don't need to expand if it already exists
return;
}
// create directories
String dirPath = new String(dstPath.toString().substring( 0,
dstPath.toString().lastIndexOf("/")));
File dir = new File(dirPath);
dir.mkdirs();
try {
synchronized (expandFileLock) {
// make sure file doesn't exist
if (f.exists()) {
return;
}
// create file
if (!f.createNewFile()) {
return;
}
FileOutputStream fos = new FileOutputStream(f);
// copy data
IOTools.flow(is, fos);
is.close();
fos.close();
}
} catch (IOException ioe) {
// delete in case file is corrupted
if (f.exists()) {
f.delete();
}
}
}
/**
* Constructs the CGI environment to be supplied to the invoked CGI
* script; relies heavliy on Servlet API methods and findCGI
*
* @param req request associated with the CGI
* invokation
*
* @return true if environment was set OK, false if there
* was a problem and no environment was set
*/
protected boolean setEnvironment(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)
*/
Hashtable envp = new Hashtable();
// Add the shell environment variables (if any)
// envp.putAll(shellEnv);
// Add the Script environment variables
String sPathInfoOrig = null;
String sPathTranslatedOrig = null;
String sPathInfo = null;
String sPathTranslated = null;
String sFullPath = null;
String sScriptName = null;
String sFullName = null;
String sName = null;
String[] sScriptNames;
sPathInfoOrig = this.pathInfo;
sPathInfoOrig = sPathInfoOrig == null ? "" : sPathInfoOrig;
sPathTranslatedOrig = req.getPathTranslated();
sPathTranslatedOrig =
sPathTranslatedOrig == null ? "" : sPathTranslatedOrig;
if (webAppRootDir == null ) {
// The app has not been deployed in exploded form
webAppRootDir = tempDir.toString();
expandScript();
}
sScriptNames = findScript(sPathInfoOrig,
webAppRootDir,
contextPath,
servletPath,
scriptPathPrefix);
sFullPath = sScriptNames[0];
sScriptName = sScriptNames[1];
sFullName = sScriptNames[2];
sName = sScriptNames[3];
if (sFullPath == null
|| sScriptName == null
|| sFullName == null
|| sName == null) {
log.error("Invalid script names");
return false;
}
envp.put("SERVER_SOFTWARE", "JBossWebServer");
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 sPort = (port == 0 ? new Integer(-1) : new Integer(port));
envp.put("SERVER_PORT", sPort.toString());
/*
* Local addres and port
*/
envp.put("LOCAL_NAME", nullsToBlanks(req.getLocalName()));
port = req.getLocalPort();
Integer iPort = (port == 0 ? new Integer(-1) : new Integer(port));
envp.put("LOCAL_PORT", iPort.toString());
envp.put("LOCAL_ADDR", nullsToBlanks(req.getLocalAddr()));
envp.put("REQUEST_METHOD", nullsToBlanks(req.getMethod()));
/*-
* PATH_INFO should be determined by using sFullName:
* 1) Let sFullName not end in a "/" (see method findScript)
* 2) Let sFullName equal the pathInfo fragment which
* corresponds to the actual script.
* 3) Thus, PATH_INFO = request.getPathInfo().substring(
* sFullName.length())
*
* (see method findScript, where the real work is done)
*
*/
if (pathInfo == null
|| (pathInfo.substring(sFullName.length()).length() <= 0)) {
sPathInfo = "";
}
else {
sPathInfo = pathInfo.substring(sFullName.length());
}
envp.put("PATH_INFO", sPathInfo);
/*-
* 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 + sPathInfo
* (unless sPathInfo is null or blank, then the CGI
* specification dictates that the PATH_TRANSLATED metavariable
* SHOULD NOT be defined.
*
*/
if (sPathInfo != null && !("".equals(sPathInfo))) {
sPathTranslated = context.getRealPath(sPathInfo);
}
else {
sPathTranslated = null;
}
if (sPathTranslated == null || "".equals(sPathTranslated)) {
// Nothing.
}
else {
envp.put("PATH_TRANSLATED", nullsToBlanks(sPathTranslated));
}
envp.put("SCRIPT_NAME", nullsToBlanks(sScriptName));
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.
*/
int contentLength = req.getContentLength();
String sContentLength = (contentLength <= 0 ? "" :
(new Integer(contentLength)).toString());
envp.put("CONTENT_LENGTH", sContentLength);
Enumeration headers = req.getHeaderNames();
String header = null;
while (headers.hasMoreElements()) {
header = null;
header = ((String)headers.nextElement()).toUpperCase();
//REMIND: rewrite multiple headers as if received as single
//REMIND: change character set
//REMIND: I forgot what the previous REMIND means
if ("AUTHORIZATION".equalsIgnoreCase(header) ||
"PROXY_AUTHORIZATION".equalsIgnoreCase(header)) {
//NOOP per CGI specification section 11.2
}
else {
envp.put("HTTP_" + header.replace('-', '_'),
req.getHeader(header));
}
}
scriptFile = new File(sFullPath);
scriptFullPath = scriptFile.getCanonicalPath();
workingDirectory = new File(scriptFullPath.substring(0,
scriptFullPath.lastIndexOf(File.separator)));
envp.put("SCRIPT_FILENAME", scriptFullPath);
envp.put("CONTEXT_PATH", nullsToBlanks(contextPath));
String self = "";
if (contextPath != null && ! "".equals(contextPath) && ! "/".equals(contextPath)) {
self = contextPath;
}
if (servletPath != null && ! "".equals(servletPath) && ! "/".equals(servletPath)) {
self = self.concat(servletPath);
}
envp.put("PHP_SELF", nullsToBlanks(self));
if (req.isSecure()) {
envp.put("HTTPS", "ON");
envp.put("SSL_CIPHER", req.getAttribute(CIPHER_SUITE));
envp.put("SSL_SESSION_ID", req.getAttribute(SSL_SESSION));
envp.put("SSL_CIPHER_USEKEYSIZE", String.valueOf(req.getAttribute(KEY_SIZE)));
X509Certificate[] certs =
(X509Certificate[])req.getAttribute(CERTIFICATE_KEY);
if (certs != null) {
// Well use the first, normaly the client certificate.
envp.put("SSL_SERVER_V_START", certs[0].getNotAfter().toString());
envp.put("SSL_SERVER_V_END", certs[0].getNotBefore().toString());
envp.put("SSL_CLIENT_A_KEY", certs[0].getSigAlgName());
// Oops getEncoded gives a DER not PEM encoded ... envp.put("SSL_CLIENT_CERT", certs[0].getEncoded());
envp.put("SSL_SERVER_M_SERIAL", certs[0].getSerialNumber().toString());
envp.put("SSL_SERVER_M_VERSION", String.valueOf(certs[0].getVersion()));
// Subject
envp.put("SSL_CLIENT_S_DN", certs[0].getSubjectX500Principal().getName());
// To fill the elements C,ST... Email
String pr = certs[0].getSubjectX500Principal().getName();
String prs[] = pr.split(", ");
for (int c = 0; c < prs.length; c++) {
String pprs[] = prs[c].split("=");
envp.put("SSL_CLIENT_S_DN_" + pprs[0], pprs[1]);
}
// Issuer
envp.put("SSL_CLIENT_I_DN", certs[0].getIssuerX500Principal().getName());
// To fill the elements C,ST... Email Still to TODO.
pr = certs[0].getSubjectX500Principal().getName();
prs = pr.split(", ");
for (int c = 0; c < prs.length; c++) {
String pprs[] = prs[c].split("=");
envp.put("SSL_CLIENT_I_DN_" + pprs[0], pprs[1]);
}
// envp.put("CERT_ISSUER",
// nullsToBlanks(certs[c].getIssuerX500Principal().getName()));
}
}
this.env = envp;
return true;
}
/**
* 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
*
*/
public ScriptEnvironment(HttpServletRequest req,
ServletContext context,
String scriptPathPrefix)
throws IOException
{
this.scriptPathPrefix = scriptPathPrefix;
this.context = context;
this.webAppRootDir = context.getRealPath("/");
this.tempDir = (File)context.getAttribute(Globals.WORK_DIR_ATTR);
if (req.getAttribute(Globals.INCLUDE_CONTEXT_PATH_ATTR) != null) {
// Include
this.contextPath = (String) req.getAttribute(Globals.INCLUDE_CONTEXT_PATH_ATTR);
this.servletPath = (String) req.getAttribute(Globals.INCLUDE_SERVLET_PATH_ATTR);
this.pathInfo = (String) req.getAttribute(Globals.INCLUDE_PATH_INFO_ATTR);
}
else {
// Direct call
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;
}
this.valid = setEnvironment(req);
}
/**
* Gets derived script full path
*
* @return full script path
*
*/
public String getFullPath()
{
return scriptFullPath;
}
/**
* Gets derived Script file
*
* @return Script file
*
*/
public File getScriptFile()
{
return scriptFile;
}
/**
* Gets derived Script working directory
*
* @return working directory
*
*/
public File getWorkingDirectory()
{
return workingDirectory;
}
/**
* Gets derived Script environment
*
* @return Script environment
*
*/
public Hashtable getEnvironment()
{
return env;
}
/**
* Gets derived Script query parameters
*
* @return Script query parameters
*
*/
public ArrayList getParameters()
{
return queryParameters;
}
/**
* Gets validity status
*
* @return true if this environment is valid, false
* otherwise
*
*/
public 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 null
*/
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 null
*/
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 null
or empty ("")
*/
protected String blanksToString(String couldBeBlank,
String subForBlanks)
{
return (("".equals(couldBeBlank) || couldBeBlank == null)
? subForBlanks
: couldBeBlank);
}
/**
* Converts Environment Hastable to String array
*
* @return Srring array containing name value pairs.
* @exception NullPointerException if a hash key has a null value
*/
public String[] getEnvironmentArray()
throws NullPointerException
{
return hashToStringArray(env);
}
/**
* Converts a Hashtable to a String array by converting each
* key/value pair in the Hashtable to two consecutive Strings
*
* @param h Hashtable to convert
* @return converted string array
* @exception NullPointerException if a hash key has a null value
*/
public static String[] hashToStringArray(Hashtable h)
throws NullPointerException
{
Vector v = new Vector();
Enumeration e = h.keys();
while (e.hasMoreElements()) {
String k = e.nextElement().toString();
v.add(k);
v.add(h.get(k));
}
String[] strArr = new String[v.size()];
v.copyInto(strArr);
return strArr;
}
}