/* * 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.cloud.loadbalance.CustomLoadBalanceManager; import com.caucho.cloud.loadbalance.LoadBalanceBuilder; import com.caucho.cloud.loadbalance.LoadBalanceManager; import com.caucho.cloud.loadbalance.LoadBalanceService; import com.caucho.config.ConfigException; import com.caucho.config.types.Period; import com.caucho.network.balance.ClientSocket; import com.caucho.network.balance.ClientSocketFactory; import com.caucho.server.cluster.Server; import com.caucho.server.webapp.WebApp; import com.caucho.util.Alarm; import com.caucho.util.CharBuffer; import com.caucho.util.FreeList; import com.caucho.util.L10N; import com.caucho.util.QDate; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.vfs.SocketStream; import com.caucho.vfs.TempBuffer; import com.caucho.vfs.WriteStream; import com.caucho.vfs.Vfs; import javax.servlet.GenericServlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.util.ArrayList; import java.util.Enumeration; import java.util.logging.Level; import java.util.logging.Logger; /** * Load balancing. * * <pre> * <servlet-mapping url-pattern='/remote/*'> * <servlet-name>com.caucho.server.http.FastCGIServlet</servlet-name> * <init-param server-address='localhost:8086'/> * </servlet-mapping> * </pre> */ public class FastCGIServlet extends GenericServlet { static final protected Logger log = Logger.getLogger(FastCGIServlet.class.getName()); static final L10N L = new L10N(FastCGIServlet.class); private static final int FCGI_BEGIN_REQUEST = 1; private static final int FCGI_ABORT_REQUEST = 2; private static final int FCGI_END_REQUEST = 3; private static final int FCGI_PARAMS = 4; private static final int FCGI_STDIN = 5; private static final int FCGI_STDOUT = 6; private static final int FCGI_STDERR = 7; private static final int FCGI_DATA = 8; private static final int FCGI_GET_VALUES = 9; private static final int FCGI_GET_VALUES_RESULT = 10; private static final int FCGI_UNKNOWNE_TYPE = 11; private static final int FCGI_RESPONDER = 1; private static final int FCGI_AUTHORIZER = 2; private static final int FCGI_FILTER = 3; private static final int FCGI_VERSION = 1; private static final int FCGI_KEEP_CONN = 1; private static final int FCGI_REQUEST_COMPLETE = 0; private static final int FCGI_CANT_MPX_CONN = 1; private static final int FCGI_OVERLOADED = 2; private static final int FCGI_UNKNOWN_ROLE = 3; private int _servletId; private LoadBalanceBuilder _loadBalanceBuilder; private LoadBalanceManager _loadBalancer; private Path _pwd; private String _hostAddress; private InetAddress _hostAddr; private int _hostPort; protected QDate _calendar = new QDate(); private long _readTimeout = 120000; private int _maxKeepaliveCount = 250; private long _keepaliveTimeout = 15000; private int _idCount = 0; public FastCGIServlet() { LoadBalanceService loadBalanceService = LoadBalanceService.getCurrent(); if (loadBalanceService == null) { throw new IllegalStateException(L.l("'{0}' requires an active {1}", this, LoadBalanceService.class.getSimpleName())); } _loadBalanceBuilder = loadBalanceService.createBuilder(); _loadBalanceBuilder.setMeterCategory("Resin|FastCGI"); } /** * Sets the host address. */ public void addAddress(String address) { _loadBalanceBuilder.addAddress(address); } public void setServerAddress(String address) { addAddress(address); } /** * Sets the fail recover time. */ public void setFailRecoverTime(Period period) { // _tcpPool.setFailRecoverTime(period); } /** * Sets the keepalive max. */ public void setMaxKeepalive(int max) { _maxKeepaliveCount = max; } /** * Sets the keepalive timeout. */ public void setKeepaliveTimeout(Period period) { _keepaliveTimeout = period.getPeriod(); } /** * Sets the socket timeout. */ public void setReadTimeout(Period timeout) { _readTimeout = timeout.getPeriod(); } /** * Initialize the servlet with the server's sruns. */ public void init(WebApp webApp) throws ServletException { init(); } /** * Initialize the servlet with the server's sruns. */ public void init() throws ServletException { _pwd = Vfs.lookup(); String serverAddress = getInitParameter("server-address"); if (serverAddress != null) addAddress(serverAddress); _loadBalancer = _loadBalanceBuilder.create(); } /** * Handle the request. */ @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; OutputStream out = res.getOutputStream(); String sessionId = null; ClientSocket stream = _loadBalancer.openSticky(sessionId, request, null); boolean isValid = false; if (stream == null) { if (log.isLoggable(Level.FINE)) log.fine(this + " cannot open a stream from " + _loadBalancer); res.sendError(503); return; } try { // fcgiSocket.setExpire(Alarm.getCurrentTime() + _keepaliveTimeout); boolean isKeepalive = true; long startRequestTime = Alarm.getCurrentTime(); if (handleRequest(req, res, stream, out, isKeepalive)) { stream.free(startRequestTime); stream = null; } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } finally { if (stream != null) stream.close(); } } private boolean handleRequest(HttpServletRequest req, HttpServletResponse res, ClientSocket stream, OutputStream out, boolean keepalive) throws ServletException, IOException { ReadStream rs = stream.getInputStream(); WriteStream ws = stream.getOutputStream(); writeHeader(ws, FCGI_BEGIN_REQUEST, 8); int role = FCGI_RESPONDER; ws.write(role >> 8); ws.write(role); ws.write(keepalive ? FCGI_KEEP_CONN : 0); // flags for (int i = 0; i < 5; i++) ws.write(0); setEnvironment(stream, ws, req); InputStream in = req.getInputStream(); TempBuffer tempBuf = TempBuffer.allocate(); byte []buf = tempBuf.getBuffer(); int len = buf.length; int sublen; writeHeader(ws, FCGI_PARAMS, 0); boolean hasStdin = false; while ((sublen = in.read(buf, 0, len)) > 0) { hasStdin = true; writeHeader(ws, FCGI_STDIN, sublen); ws.write(buf, 0, sublen); } TempBuffer.free(tempBuf); tempBuf = null; /* if (hasStdin) writeHeader(fcgiSocket, ws, FCGI_STDIN, 0); */ writeHeader(ws, FCGI_STDIN, 0); ws.flush(); FastCGIInputStream is = new FastCGIInputStream(stream); int ch = parseHeaders(res, is); if (ch >= 0) out.write(ch); TempBuffer tb = TempBuffer.allocate(); byte []buffer = tb.getBuffer(); while ((sublen = is.read(buffer, 0, buffer.length)) > 0) { out.write(buffer, 0, sublen); } TempBuffer.free(tb); return ! is.isDead() && keepalive; } private void setEnvironment(ClientSocket stream, WriteStream ws, HttpServletRequest req) throws IOException { addHeader(stream, ws, "REQUEST_URI", req.getRequestURI()); addHeader(stream, ws, "REQUEST_METHOD", req.getMethod()); addHeader(stream, ws, "SERVER_SOFTWARE", "Resin/" + VersionFactory.getVersion()); addHeader(stream, ws, "SERVER_NAME", req.getServerName()); //addHeader(stream, ws, "SERVER_ADDR=" + req.getServerAddr()); addHeader(stream, ws, "SERVER_PORT", String.valueOf(req.getServerPort())); addHeader(stream, ws, "REMOTE_ADDR", req.getRemoteAddr()); addHeader(stream, ws, "REMOTE_HOST", req.getRemoteAddr()); // addHeader(stream, ws, "REMOTE_PORT=" + req.getRemotePort()); if (req.getRemoteUser() != null) addHeader(stream, ws, "REMOTE_USER", req.getRemoteUser()); else addHeader(stream, ws, "REMOTE_USER", ""); if (req.getAuthType() != null) addHeader(stream, ws, "AUTH_TYPE", req.getAuthType()); addHeader(stream, ws, "GATEWAY_INTERFACE", "CGI/1.1"); addHeader(stream, ws, "SERVER_PROTOCOL", req.getProtocol()); if (req.getQueryString() != null) addHeader(stream, ws, "QUERY_STRING", req.getQueryString()); else addHeader(stream, ws, "QUERY_STRING", ""); String scriptPath = req.getServletPath(); String pathInfo = req.getPathInfo(); WebApp webApp = (WebApp) req.getServletContext(); Path appDir = webApp.getRootDirectory(); String realPath = webApp.getRealPath(scriptPath); if (! appDir.lookup(realPath).isFile() && pathInfo != null) scriptPath = scriptPath + pathInfo; /* * FastCGI (specifically quercus) uses the PATH_INFO and PATH_TRANSLATED * for the script path. */ log.finer("STREAM file: " + webApp.getRealPath(scriptPath)); addHeader(stream, ws, "PATH_INFO", req.getContextPath() + scriptPath); addHeader(stream, ws, "PATH_TRANSLATED", webApp.getRealPath(scriptPath)); /* These are the values which would be sent to CGI. addHeader(stream, ws, "SCRIPT_NAME", req.getContextPath() + scriptPath); addHeader(stream, ws, "SCRIPT_FILENAME", app.getRealPath(scriptPath)); if (pathInfo != null) { addHeader(stream, ws, "PATH_INFO", pathInfo); addHeader(stream, ws, "PATH_TRANSLATED", req.getRealPath(pathInfo)); } else { addHeader(stream, ws, "PATH_INFO", ""); addHeader(stream, ws, "PATH_TRANSLATED", ""); } */ int contentLength = req.getContentLength(); if (contentLength < 0) addHeader(stream, ws, "CONTENT_LENGTH", "0"); else addHeader(stream, ws, "CONTENT_LENGTH", String.valueOf(contentLength)); ServletContext rootContext = webApp.getContext("/"); if (rootContext != null) addHeader(stream, ws, "DOCUMENT_ROOT", rootContext.getRealPath("/")); CharBuffer cb = new CharBuffer(); Enumeration e = req.getHeaderNames(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); String value = req.getHeader(key); if (key.equalsIgnoreCase("content-length")) addHeader(stream, ws, "CONTENT_LENGTH", value); else if (key.equalsIgnoreCase("content-type")) addHeader(stream, ws, "CONTENT_TYPE", value); else if (key.equalsIgnoreCase("if-modified-since")) { } else if (key.equalsIgnoreCase("if-none-match")) { } else if (key.equalsIgnoreCase("authorization")) { } else if (key.equalsIgnoreCase("proxy-authorization")) { } else addHeader(stream, ws, convertHeader(cb, key), value); } } private CharBuffer convertHeader(CharBuffer cb, String key) { cb.clear(); 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); } return cb; } private int parseHeaders(HttpServletResponse res, InputStream is) throws IOException { CharBuffer key = new CharBuffer(); CharBuffer value = new CharBuffer(); int ch = is.read(); if (ch < 0) { log.fine("Can't contact FastCGI"); res.sendError(404); return -1; } while (ch >= 0) { key.clear(); value.clear(); for (; ch >= 0 && ch != ' ' && ch != '\r' && ch != '\n' && ch != ':'; ch = is.read()) { key.append((char) ch); } for (; ch >= 0 && ch == ' ' || ch == ':'; ch = is.read()) { } for (; ch >= 0 && ch != '\r' && ch != '\n'; ch = is.read()) { value.append((char) ch); } if (ch == '\r') { ch = is.read(); if (ch == '\n') ch = is.read(); } if (key.length() == 0) return ch; if (log.isLoggable(Level.FINE)) log.fine("fastcgi:" + key + ": " + value); if (key.equalsIgnoreCase("status")) { int status = 0; int len = value.length(); for (int i = 0; i < len; i++) { char digit = value.charAt(i); if ('0' <= digit && digit <= '9') status = 10 * status + digit - '0'; else break; } res.setStatus(status); } else if (key.startsWith("http") || key.startsWith("HTTP")) { } else if (key.equalsIgnoreCase("location")) { res.sendRedirect(value.toString()); } else res.addHeader(key.toString(), value.toString()); } return ch; } private void addHeader(ClientSocket stream, WriteStream ws, String key, String value) throws IOException { if (value == null) return; int keyLen = key.length(); int valLen = value.length(); int len = keyLen + valLen; if (keyLen < 0x80) len += 1; else len += 4; if (valLen < 0x80) len += 1; else len += 4; writeHeader(ws, FCGI_PARAMS, len); if (keyLen < 0x80) ws.write(keyLen); else { ws.write(0x80 | (keyLen >> 24)); ws.write(keyLen >> 16); ws.write(keyLen >> 8); ws.write(keyLen); } if (valLen < 0x80) ws.write(valLen); else { ws.write(0x80 | (valLen >> 24)); ws.write(valLen >> 16); ws.write(valLen >> 8); ws.write(valLen); } ws.print(key); ws.print(value); } private void addHeader(ClientSocket stream, WriteStream ws, CharBuffer key, String value) throws IOException { int keyLen = key.getLength(); int valLen = value.length(); int len = keyLen + valLen; if (keyLen < 0x80) len += 1; else len += 4; if (valLen < 0x80) len += 1; else len += 4; writeHeader(ws, FCGI_PARAMS, len); if (keyLen < 0x80) ws.write(keyLen); else { ws.write(0x80 | (keyLen >> 24)); ws.write(keyLen >> 16); ws.write(keyLen >> 8); ws.write(keyLen); } if (valLen < 0x80) ws.write(valLen); else { ws.write(0x80 | (valLen >> 24)); ws.write(valLen >> 16); ws.write(valLen >> 8); ws.write(valLen); } ws.print(key.getBuffer(), 0, keyLen); ws.print(value); } private void writeHeader(WriteStream ws, int type, int length) throws IOException { int id = 1; int pad = 0; ws.write(FCGI_VERSION); ws.write(type); ws.write(id >> 8); ws.write(id); ws.write(length >> 8); ws.write(length); ws.write(pad); ws.write(0); } public void destroy() { _loadBalancer.close(); } static class FastCGIInputStream extends InputStream { private ClientSocket _stream; private InputStream _is; private int _chunkLength; private int _padLength; private boolean _isDead; public FastCGIInputStream() { } public FastCGIInputStream(ClientSocket stream) { init(stream); } public void init(ClientSocket stream) { _stream = stream; _is = stream.getInputStream(); _chunkLength = 0; _isDead = false; } public boolean isDead() { return _isDead; } public int read() throws IOException { do { if (_chunkLength > 0) { _chunkLength--; return _is.read(); } } while (readNext()); return -1; } private boolean readNext() throws IOException { if (_is == null) return false; if (_padLength > 0) { _is.skip(_padLength); _padLength = 0; } int version; while ((version = _is.read()) >= 0) { int type = _is.read(); int id = (_is.read() << 8) + _is.read(); int length = (_is.read() << 8) + _is.read(); int padding = _is.read(); _is.read(); switch (type) { case FCGI_END_REQUEST: { int appStatus = ((_is.read() << 24) + (_is.read() << 16) + (_is.read() << 8) + (_is.read())); int pStatus = _is.read(); if (log.isLoggable(Level.FINER)) { log.finer(_stream + ": FCGI_END_REQUEST(appStatus:" + appStatus + ", pStatus:" + pStatus + ")"); } if (appStatus != 0) _isDead = true; if (pStatus != FCGI_REQUEST_COMPLETE) _isDead = true; _is.skip(3); _is = null; return false; } case FCGI_STDOUT: if (log.isLoggable(Level.FINER)) { log.finer(_stream + ": FCGI_STDOUT(length:" + length + ", padding:" + padding + ")"); } if (length == 0) { if (padding > 0) _is.skip(padding); break; } else { _chunkLength = length; _padLength = padding; return true; } case FCGI_STDERR: if (log.isLoggable(Level.FINER)) { log.finer(_stream + ": FCGI_STDERR(length:" + length + ", padding:" + padding + ")"); } byte []buf = new byte[length]; _is.read(buf, 0, length); log.warning(new String(buf, 0, length)); if (padding > 0) _is.skip(padding); break; default: log.warning(_stream + ": Unknown Protocol(" + type + ")"); _isDead = true; _is.skip(length + padding); break; } } _isDead = true; return false; } } }