sunlabs.brazil.handler.CgiHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sunlabs.brazil Show documentation
Show all versions of sunlabs.brazil Show documentation
Extremely small footprint Java HTTP stack.
The newest version!
/*
* CgiHandler.java
*
* Brazil project web application toolkit,
* export version: 2.3
* Copyright (c) 1998-2006 Sun Microsystems, Inc.
*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is included as the file "license.terms",
* and also available at http://www.sun.com/
*
* The Original Code is from:
* Brazil project web application toolkit release 2.3.
* The Initial Developer of the Original Code is: suhler.
* Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): cstevens, drach, suhler.
*
* Version: 2.4
* Created by suhler on 98/09/14
* Last modified by suhler on 06/06/14 10:26:09
*
* Version Histories:
*
* 2.4 06/06/14-10:26:09 (suhler)
* added "runwith" and "hoheaders" to allow apache mod_xxx simulation
* .
*
* 2.3 05/11/27-18:26:21 (suhler)
* allow configuration choice of current -vs- original url
*
* 2.2 05/07/12-10:34:58 (suhler)
* check for HTTPS was wrong
*
* 2.1 02/10/01-16:36:22 (suhler)
* version change
*
* 1.22 02/05/02-11:15:33 (suhler)
* doc fixes, version updatew
*
* 1.21 02/05/02-08:48:30 (drach)
* Choose Runtime.exec method at run time
*
* 1.20 02/02/25-08:58:08 (suhler)
* use "url.orig" instead of request.url if available.
*
* 1.19 01/03/13-09:46:19 (suhler)
* added comments to show how to fix "CHDIR" bug when running under jdk1.3
* .
*
* 1.18 01/03/13-09:18:45 (suhler)
* return server error if CGI script fails
*
* 1.17 00/12/11-13:26:51 (suhler)
* add class=props for automatic property extraction
*
* 1.16 00/10/31-10:17:46 (suhler)
* doc fixes
*
* 1.15 00/10/05-21:17:56 (suhler)
* reject cgi scripts that have bad output
*
* 1.14 00/05/31-13:45:33 (suhler)
*
* 1.13 00/05/22-14:03:27 (suhler)
* doc updates
*
* 1.12 00/04/20-11:48:07 (cstevens)
* copyright.
*
* 1.11 99/12/07-10:54:33 (suhler)
* fixed script name
*
* 1.10 99/10/26-17:07:45 (cstevens)
* Get rid of public variables Request.server and Request.sock
* In all cases, Request.server was not necessary in the Handler.
* Request.sock was changed to Request.getSock(); it is still rarely used for
* diagnostics and logging (e.g., ChainSawHandler).
*
* 1.9 99/10/12-10:03:25 (suhler)
* temporary fix to use new mimeheader semantics
*
* 1.8 99/09/15-14:38:59 (cstevens)
* Rewritign http server to make it easier to proxy requests.
*
* 1.7 99/04/22-12:51:57 (suhler)
* Added "custom" property to enable placing handler properties into
* the cgi script environment
*
* 1.6 99/03/30-09:29:11 (suhler)
* documentation update
*
* 1.5 99/01/06-08:31:37 (suhler)
* fixed version string
*
* 1.4 98/10/22-10:44:32 (suhler)
* fixed bug that required a training /
*
* 1.3 98/10/22-09:23:25 (suhler)
* Added better diagnostics.
* There is still a bug that requires cgi scripts to end in a "/"
* - will be fixed soon
*
* 1.2 98/09/21-14:53:36 (suhler)
* changed the package names
*
* 1.2 98/09/14-18:03:03 (Codemgr)
* SunPro Code Manager data about conflicts, renames, etc...
* Name history : 2 1 handlers/CgiHandler.java
* Name history : 1 0 CgiHandler.java
*
* 1.1 98/09/14-18:03:02 (suhler)
* date and time created 98/09/14 18:03:02 by suhler
*
*/
package sunlabs.brazil.handler;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.StringTokenizer;
import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.FileHandler;
import sunlabs.brazil.server.Server;
/**
* Handler for implementing cgi/1.1 interface.
* This implementation allows either suffix matching (e.g. .cgi) to identify
* cgi scripts, or prefix matching (e.g. /bgi-bin). Defaults to "/".
* All output from the cgi script is buffered (e.g. chunked encoding is
* not supported).
*
* NOTE: in versions of Java prior to release 1.3, the ability to set
* a working directory when running an external process is missing.
* This handler automatically checks for this ability and sets the
* proper working directory, but only if the underlying VM supports it.
*
* The following request properties are used:
*
* - root
- The document root for cgi files
*
- suffix
- The suffix for cgi files (defaults to .cgi)
*
- prefix
- The prefix for all cgi files (e.g. /cgi-bin)
*
- url
- "o(riginal)" or "c(urrent)".
* If an upstream handler has changed the URL, this specifes
* which url to look for the cgi script relative to. The default
* is to use the original url.
*
- custom
- set to "true" to enable custom environment variables.
* If set, all server properties starting with this handler's
* prefix are placed into the environment with the name:
*
CONFIG_name
, where name is the
* property key, in upper case, with the prefix removed.
* This allows cgi scripts to be customized in the server's
* configuration file.
* - runwith
- The command to use to run scripts.
* The absolute file path is added as the last
* parameter. If not specified, the file name is run as the
* command.
*
- noheaders
-
* According to the CGI spec, cgi documents are to begin with
* properly formed http headers to specifie the type, return status
* and optionally other meta information about the request.
* if "noheaders" is specified, then the content is expected
* to *not* have any http headers, and the content type is
* as implied by the url suffix.
*
* See example configuration in the samples included with the distribution.
*
* @author Stephen Uhler
* @version 2.4, 06/06/14
*/
public class CgiHandler implements Handler {
private String propsPrefix; // string prefix in properties table
private int port; // The listening port
private String protocol; // the access protocol http/https
private String hostname; // My hostname
private static final String ROOT = "root"; // property for document root
private static final String SUFFIX = "suffix"; // property for suffix string
private static final String PREFIX = "prefix"; // All cgi scripts must start with this
private static final String CUSTOM = "custom"; // add custom query variables
private static final String URL = "url"; // how to map url to file
private static String software = "Mini Java CgiHandler 0.3";
private static Hashtable envMap; // environ maps
/**
* construct table of CGI environment variables that need special handling
*/
static {
envMap = new Hashtable(2);
envMap.put("content-length", "CONTENT_LENGTH");
envMap.put("content-type", "CONTENT_TYPE");
}
public CgiHandler() {}
/**
* One time initialization. The handler configuration properties are
* extracted and set in
* {@link #respond(Request)} to allow
* upstream handlers to modify the parameters.
*/
public boolean
init(Server server, String prefix)
{
propsPrefix = prefix;
port = server.listen.getLocalPort();
hostname = server.hostName;
protocol = server.protocol;
return true;
}
/**
* Dispatch and handle the CGI request. Gets called on ALL requests.
* Set up the environment, exec the process, and deal
* appropriately with the input and output.
*
* In this implementation, all cgi script files must end with a
* standard suffix, although the suffix may omitted from the url.
* The url /main/do/me/too?a=b will look, starting in DocRoot,
* for main.cgi, main/do.cgi, etc until a matching file is found.
*
* Input parameters examined in the request properties:
*
* - Suffix
- The suffix for all cgi scripts (defaults to .cgi)
*
- DocRoot
- The document root, for locating the script.
*
*/
public boolean
respond(Request request)
{
String[] command; // The command to run
Process cgi; // The result of the cgi process
// Find the cgi script associated with this request.
// + turn the url into a file name
// + search path until a script is found
String which = request.props.getProperty(propsPrefix + URL, "o");
String prefix = request.props.getProperty(propsPrefix + PREFIX, "/");
String url;
if (which.indexOf("c") >= 0) {
url = request.url;
} else {
url = request.props.getProperty("url.orig");
}
if (!url.startsWith(prefix)) {
return false;
}
boolean needheaders =
request.props.getProperty(propsPrefix + "noheaders") == null;
boolean useCustom = !request.props.getProperty(propsPrefix +
CUSTOM, "").equals("");
String suffix = request.props.getProperty(propsPrefix + SUFFIX, ".cgi");
String root = request.props.getProperty(propsPrefix + ROOT,
request.props.getProperty(ROOT, "."));
request.log(Server.LOG_DIAGNOSTIC,propsPrefix + " suffix=" + suffix +
" root=" + root + " url: " + url);
int start = 1;
int end = 0;
File name = null;
while (end < url.length()) {
end = url.indexOf(File.separatorChar, start);
if (end < 0) {
end = url.length();
}
String s = url.substring(1, end);
if (!s.endsWith(suffix)) {
s += suffix;
}
name = new File(root, s);
request.log(Server.LOG_DIAGNOSTIC,propsPrefix + " looking for: " +
name);
if (name.isFile()) {
break;
}
name = null;
start = end+1;
}
if (name == null) {
return false;
}
/*
* If there is a single query parameter but no value, then tack
* it on as an additional argument (is this right?)
*/
boolean extra = (request.query.indexOf("=") == -1);
String runwith = request.props.getProperty(propsPrefix + "runwith");
String path = name.getAbsolutePath();
String type = FileHandler.getMimeType(path, request.props, propsPrefix);
int i = 0;
if (runwith != null) {
request.log(Server.LOG_DIAGNOSTIC,propsPrefix + " command= " +
runwith + " " + path);
StringTokenizer st = new StringTokenizer(runwith);
command = new String[st.countTokens() + (extra ? 2: 1)];
while (st.hasMoreTokens()) {
command[i++] = st.nextToken();
}
} else {
command = new String[extra ? 2 : 1];
request.log(Server.LOG_DIAGNOSTIC,propsPrefix+" command= " + path);
}
command[i] = path;
if (extra) {
command[i+1] = request.query;
}
/*
* Build the environment string. First, get all the http headers
* most are transferred directly to the environment, some are
* handled specially. Multiple headers with the same name are not
* handled properly.
*/
Vector env = new Vector();
Enumeration keys = request.headers.keys();
while(keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String special = (String) envMap.get(key.toLowerCase());
if (special != null) {
env.addElement(special + "=" + request.headers.get(key));
} else {
env.addElement("HTTP_"
+ key.toUpperCase().replace('-','_')
+ "="
+ request.headers.get(key));
}
}
// Add in the rest of them
env.addElement("GATEWAY_INTERFACE=CGI/1.1");
env.addElement("SERVER_SOFTWARE=" + software);
env.addElement("SERVER_NAME=" + hostname);
env.addElement("PATH_INFO=" + url.substring(end));
String pre = url.substring(0,end);
if (pre.endsWith(suffix)) {
env.addElement("SCRIPT_NAME=" + pre);
} else {
env.addElement("SCRIPT_NAME=" + pre + suffix);
}
env.addElement("SERVER_PORT=" + port);
env.addElement("REMOTE_ADDR=" +
request.getSocket().getInetAddress().getHostAddress());
env.addElement("PATH_TRANSLATED=" + root + url.substring(end));
env.addElement("REQUEST_METHOD=" + request.method);
env.addElement("SERVER_PROTOCOL=" + request.protocol);
env.addElement("QUERY_STRING=" + request.query);
String serverUrl = request.serverUrl();
env.addElement("SERVER_URL=" + serverUrl);
if (serverUrl.startsWith("https:")) {
env.addElement("HTTPS=on");
}
/*
* add in the "custom" environment variables, if requested
*/
if (useCustom) {
int len = propsPrefix.length();
keys = request.props.propertyNames();
while(keys.hasMoreElements()) {
String key = (String) keys.nextElement();
if (key.startsWith(propsPrefix)) {
env.addElement("CONFIG_" +
key.substring(len).toUpperCase() +
"=" + request.props.getProperty(key, null));
}
}
env.addElement("CONFIG_PREFIX=" + propsPrefix);
}
String environ[] = new String[env.size()];
env.copyInto(environ);
request.log(Server.LOG_DIAGNOSTIC,propsPrefix + " ENV= " + env);
// Run the script
try {
cgi = exec(command, environ, new File(name.getParent()));
DataInputStream in = new DataInputStream(
new BufferedInputStream(cgi.getInputStream()));
// If we have data, send it to the process
if (request.postData != null) {
OutputStream toGci = cgi.getOutputStream();
toGci.write(request.postData, 0, request.postData.length);
toGci.close();
}
// Now get the output of the cgi script. Start by reading the
// "mini header", then just copy the rest
String head;
type = (type==null ? "text/html" : type);
int status = 200;
while(needheaders) {
head = in.readLine();
if (head==null || head.length() == 0) {
break;
}
int colonIndex = head.indexOf(':');
if (colonIndex < 0) {
request.sendError(500, "Missing header from cgi output");
return true;
}
String lower = head.toLowerCase();
if (lower.startsWith("status:")) {
try {
status = Integer.parseInt(
head.substring(colonIndex+1).trim());
} catch (NumberFormatException e) {}
} else if (lower.startsWith("content-type:")) {
type = head.substring(colonIndex+1).trim();
} else if (lower.startsWith("location:")) {
status = 302;
request.addHeader(head);
} else {
request.addHeader(head);
}
}
/*
* Now copy the rest of the data into a buffer, so we can count it.
* we should be doing chunked encoding for 1.1 capable clients XXX
*/
ByteArrayOutputStream buff = new ByteArrayOutputStream();
int c;
while((c = in.read()) >= 0) {
buff.write(c);
}
request.sendHeaders(status, type, buff.size());
buff.writeTo(request.out);
request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
"Cgi output " + buff.size());
cgi.waitFor();
} catch (Exception e) {
// System.out.println("oops: " + e);
//e.printStackTrace();
request.sendError(500, "CGI failure", e.getMessage());
}
return true;
}
private int params;
private Method execMethod;
private void search() throws Exception {
Method[] m = Runtime.class.getDeclaredMethods();
int n = -1;
for (int i = 0; i < m.length; i++) {
if (m[i].getName().equals("exec")) {
Class[] c = m[i].getParameterTypes();
if (c.length == 3 && c[0] == String[].class) {
// we have 3 arg exec for sure
params = 3;
n = i;
break;
}
if (c.length == 2 && c[0] == String[].class) {
// let's save it in case we need it, but keep looking
params = 2;
n = i;
}
}
}
if (n == -1) {
throw new Exception("No method exec(String[], ...) found in Runtime");
}
execMethod = m[n];
}
private Object[] args;
private Process exec(String[] cmd, String[] envp, File dir)
throws Exception
{
if (execMethod == null) {
search();
if (params == 2) {
args = new Object[2];
} else {
args = new Object[3];
}
}
args[0] = cmd;
args[1] = envp;
if (params == 3) {
args[2] = dir;
}
return (Process)execMethod.invoke(Runtime.getRuntime(), args);
}
}