/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source 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, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.servlets;
import com.caucho.VersionFactory;
import com.caucho.util.Alarm;
import com.caucho.util.AlarmListener;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.TempBuffer;
import com.caucho.vfs.Vfs;
import javax.servlet.GenericServlet;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* CGI
*/
public class CGIServlet extends GenericServlet {
static protected final Logger log
= Logger.getLogger(CGIServlet.class.getName());
static final L10N L = new L10N(CGIServlet.class);
private String _executable;
private boolean _stderrIsException = true;
private boolean _ignoreExitCode = false;
/**
* Sets an executable to run the script.
*/
public void setExecutable(String executable)
{
_executable = executable;
}
public void setStderrIsException(boolean isException)
{
_stderrIsException = isException;
}
/**
* If true, do not treat a non-zero exit code as an error, default false.
*/
public void setIgnoreExitCode(boolean ignoreExitCode)
{
_ignoreExitCode = ignoreExitCode;
}
/**
* Handle the request.
*/
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException
{
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String requestURI;
String contextPath;
String servletPath;
String servletPathInfo;
String queryString;
requestURI
= (String) req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestURI != null) {
contextPath
= (String) req.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH);
servletPath
= (String) req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
servletPathInfo
= (String) req.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
queryString
= (String) req.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
}
else {
requestURI = req.getRequestURI();
contextPath = req.getContextPath();
servletPath = req.getServletPath();
servletPathInfo = req.getPathInfo();
queryString = req.getQueryString();
}
String scriptPath;
String pathInfo;
if (servletPathInfo == null) {
scriptPath = servletPath;
pathInfo = null;
}
else {
String fullPath = servletPath + servletPathInfo;
int i = findScriptPathIndex(req, fullPath);
if (i < 0) {
if (log.isLoggable(Level.FINE))
log.fine(L.l("no script path index for `{0}'", fullPath));
res.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
scriptPath = fullPath.substring(0, i);
pathInfo = fullPath.substring(i);
if ("".equals(pathInfo))
pathInfo = null;
}
String realPath = getServletContext().getRealPath(scriptPath);
Path vfsPath = Vfs.lookup(realPath);
if (! vfsPath.canRead() || vfsPath.isDirectory()) {
if (log.isLoggable(Level.FINE))
log.fine(L.l("script '{0}' is unreadable", vfsPath));
res.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String []env = createEnvironment(req, requestURI, contextPath,
scriptPath, pathInfo, queryString);
String []args = getArgs(realPath);
if (log.isLoggable(Level.FINER)) {
if (args.length > 1)
log.finer("[cgi] exec " + args[0] + " " + args[1]);
else if (args.length > 0)
log.finer("[cgi] exec " + args[0]);
}
Runtime runtime = Runtime.getRuntime();
Process process = null;
Alarm alarm = null;
try {
File dir = new File(Vfs.lookup(realPath).getParent().getNativePath());
if (log.isLoggable(Level.FINE)) {
CharBuffer argsBuf = new CharBuffer();
argsBuf.append('[');
for (String arg : args) {
if (argsBuf.length() > 1)
argsBuf.append(", ");
argsBuf.append('"');
argsBuf.append(arg);
argsBuf.append('"');
}
argsBuf.append(']');
log.fine(L.l("exec {0} (pwd={1})", argsBuf, dir));
if (log.isLoggable(Level.FINEST)) {
for (String envElement : env)
log.finest(envElement);
}
}
process = runtime.exec(args, env, dir);
InputStream inputStream = process.getInputStream();
InputStream errorStream = process.getErrorStream();
TimeoutAlarm timeout;
timeout = new TimeoutAlarm(requestURI, process, inputStream);
alarm = new Alarm(timeout, 360 * 1000);
OutputStream outputStream = process.getOutputStream();
TempBuffer tempBuf = TempBuffer.allocate();
byte []buf = tempBuf.getBuffer();
try {
ServletInputStream sis = req.getInputStream();
int len;
while ((len = sis.read(buf, 0, buf.length)) > 0) {
outputStream.write(buf, 0, len);
}
outputStream.flush();
} catch (IOException e) {
log.log(Level.FINER, e.toString(), e);
} finally {
outputStream.close();
}
TempBuffer.free(tempBuf);
tempBuf = null;
ReadStream rs = Vfs.openRead(inputStream);
boolean hasStatus = false;
try {
hasStatus = parseHeaders(req, res, rs);
OutputStream out = res.getOutputStream();
rs.writeToStream(out);
} finally {
try {
rs.close();
} catch (Throwable e) {
log.log(Level.FINER, e.toString(), e);
}
inputStream.close();
}
StringBuilder error = new StringBuilder();
boolean hasContent = false;
int ch;
while (errorStream.available() > 0 && (ch = errorStream.read()) > 0) {
error.append((char) ch);
if (! Character.isWhitespace((char) ch))
hasContent = true;
}
errorStream.close();
if (hasContent) {
String errorString = error.toString();
log.warning(errorString);
if (! hasStatus && _stderrIsException)
throw new ServletException(errorString);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
if (hasStatus) {
if (log.isLoggable(Level.FINER))
log.finer(L.l("exit code {0} (ignored, hasStatus)", exitCode));
}
else if (_ignoreExitCode) {
if (log.isLoggable(Level.FINER))
log.finer(L.l("exit code {0} (ignored)", exitCode));
}
else
throw new ServletException(L.l("CGI execution failed. Exit code {0}",
exitCode));
}
} catch (IOException e) {
throw e;
} catch (ServletException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
} finally {
if (alarm != null)
alarm.dequeue();
try {
process.destroy();
} catch (Throwable e) {
}
}
}
/**
* Returns the index to the script path.
*/
private int findScriptPathIndex(HttpServletRequest req, String fullPath)
{
String realPath = req.getRealPath(fullPath);
Path path = Vfs.lookup(realPath);
if (log.isLoggable(Level.FINER))
log.finer(L.l("real-path is `{0}'", path));
if (path.canRead() && ! path.isDirectory())
return fullPath.length();
int tail = fullPath.length();
int head;
while ((head = fullPath.lastIndexOf('/', tail)) >= 0) {
String subPath = fullPath.substring(0, head);
realPath = req.getRealPath(subPath);
path = Vfs.lookup(realPath);
if (log.isLoggable(Level.FINEST))
log.finest(L.l("trying script path {0}", path));
if (path.canRead() && ! path.isDirectory())
return head;
tail = head - 1;
}
return -1;
}
private String []getArgs(String path)
{
if (_executable != null)
return new String[] { _executable, path };
ReadStream is = null;
try {
is = Vfs.lookup(path).openRead();
int ch;
if (is.read() != '#')
return new String[] { path };
else if (is.read() != '!')
return new String[] { path };
CharBuffer cb = CharBuffer.allocate();
ArrayList<String> list = new ArrayList<String>();
ch = is.read();
while ((ch >= 0 && ch != '\r' && ch != '\n')) {
for (; ch == ' ' || ch == '\t'; ch = is.read()) {
}
if (ch < 0 || ch == '\r' || ch == '\n') {
if (list.size() > 0) {
list.add(path);
return list.toArray(new String[list.size()]);
}
else
return new String[] { path };
}
cb.clear();
while (ch > 0 && ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
cb.append((char) ch);
ch = is.read();
}
list.add(cb.toString());
for (; ch == ' ' || ch == '\t'; ch = is.read()) {
}
}
if (list.size() > 0) {
list.add(path);
return list.toArray(new String[list.size()]);
}
else
return new String[] { path };
} catch (Exception e) {
return new String[] { path };
} finally {
if (is != null) {
is.close();
}
}
}
private String[] createEnvironment(HttpServletRequest req,
String requestURI, String contextPath,
String scriptPath, String pathInfo,
String queryString)
{
boolean isFine = log.isLoggable(Level.FINE);
ArrayList<String> env = new ArrayList<String>();
env.add("SERVER_SOFTWARE=Resin/" + VersionFactory.getVersion());
env.add("SERVER_NAME=" + req.getServerName());
env.add("REDIRECT_STATUS=200");
//env.add("SERVER_ADDR=" + req.getServerAddr());
env.add("SERVER_PORT=" + req.getServerPort());
env.add("REMOTE_ADDR=" + req.getRemoteAddr());
// env.add("REMOTE_PORT=" + req.getRemotePort());
if (req.getRemoteUser() != null)
env.add("REMOTE_USER=" + req.getRemoteUser());
if (req.getAuthType() != null)
env.add("AUTH_TYPE=" + req.getAuthType());
env.add("GATEWAY_INTERFACE=CGI/1.1");
env.add("SERVER_PROTOCOL=" + req.getProtocol());
env.add("REQUEST_METHOD=" + req.getMethod());
if (isFine)
log.fine("[cgi] REQUEST_METHOD=" + req.getMethod());
if (queryString != null) {
env.add("QUERY_STRING="+ queryString);
if (isFine)
log.fine("[cgi] QUERY_STRING=" + queryString);
}
env.add("REQUEST_URI=" + requestURI);
if (isFine)
log.fine("[cgi] REQUEST_URI=" + requestURI);
// PHP needs SCRIPT_FILENAME or it reports "No input file specified."
env.add("SCRIPT_FILENAME=" + req.getRealPath(scriptPath));
scriptPath = contextPath + scriptPath;
env.add("SCRIPT_NAME=" + scriptPath);
if (isFine)
log.fine("[cgi] SCRIPT_NAME=" + scriptPath);
if (pathInfo != null) {
env.add("PATH_INFO=" + pathInfo);
env.add("PATH_TRANSLATED=" + req.getRealPath(pathInfo));
}
Enumeration e = req.getHeaderNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String value = req.getHeader(key);
if (isFine)
log.fine("[cgi] " + key + "=" + value);
if (key.equalsIgnoreCase("content-length"))
env.add("CONTENT_LENGTH=" + value);
else if (key.equalsIgnoreCase("content-type"))
env.add("CONTENT_TYPE=" + value);
else if (key.equalsIgnoreCase("authorization")) {
}
else if (key.equalsIgnoreCase("proxy-authorization")) {
}
else
env.add(convertHeader(key, value));
}
return (String []) env.toArray(new String[env.size()]);
}
private String convertHeader(String key, String value)
{
CharBuffer cb = new CharBuffer();
cb.append("HTTP_");
for (int i = 0; i < key.length(); i++) {
char ch = key.charAt(i);
if (ch == '-')
cb.append('_');
else if (ch >= 'a' && ch <= 'z')
cb.append((char) (ch + 'A' - 'a'));
else
cb.append(ch);
}
cb.append('=');
cb.append(value);
return cb.close();
}
private boolean parseHeaders(HttpServletRequest req,
HttpServletResponse res,
ReadStream rs)
throws IOException
{
boolean hasStatus = false;
CharBuffer key = new CharBuffer();
CharBuffer value = new CharBuffer();
int ch;
while (true) {
key.clear();
value.clear();
for (ch = rs.read();
ch >= 0 && ch != ' ' && ch != '\r' && ch != '\n' && ch != ':';
ch = rs.read()) {
key.append((char) ch);
}
for (;
ch >= 0 && ch == ' ' || ch == ':';
ch = rs.read()) {
}
for (;
ch >= 0 && ch != '\r' && ch != '\n';
ch = rs.read()) {
value.append((char) ch);
}
if (ch == '\r') {
ch = rs.read();
if (ch != '\n')
rs.unread();
}
if (key.length() == 0)
return hasStatus;
String keyStr = key.toString();
String valueStr = value.toString();
if (log.isLoggable(Level.FINER))
log.finer(keyStr + ": " + valueStr);
if (keyStr.equalsIgnoreCase("Status")) {
int status = 0;
int len = valueStr.length();
int i = 0;
hasStatus = true;
for (; i < len && (ch = valueStr.charAt(i)) >= '0' && ch <= '9'; i++)
status = 10 * status + ch - '0';
for (; i < len && (ch = valueStr.charAt(i)) == ' '; i++) {
}
if (status < 304)
res.setStatus(status);
else
res.sendError(status, valueStr.substring(i));
}
else if (keyStr.equalsIgnoreCase("Location")) {
String uri;
if (valueStr.startsWith("/"))
uri = req.getContextPath() + valueStr;
else
uri = valueStr;
res.setHeader("Location", res.encodeRedirectURL(uri));
}
else
res.addHeader(keyStr, valueStr);
}
}
class TimeoutAlarm implements AlarmListener {
String _uri;
Process _process;
InputStream _is;
TimeoutAlarm(String uri, Process process, InputStream is)
{
_uri = uri;
_process = process;
_is = is;
}
public void handleAlarm(Alarm alarm)
{
log.warning("timing out CGI process for '" + _uri + "'");
try {
_is.close();
} catch (Throwable e) {
log.log(Level.WARNING, e.toString(), e);
}
try {
_process.destroy();
} catch (Throwable e) {
log.log(Level.WARNING, e.toString(), e);
}
}
}
}