/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone.ajp13; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import winstone.HostGroup; import winstone.Launcher; import winstone.Listener; import winstone.Logger; import winstone.ObjectPool; import winstone.RequestHandlerThread; import winstone.WebAppConfiguration; import winstone.WinstoneException; import winstone.WinstoneInputStream; import winstone.WinstoneOutputStream; import winstone.WinstoneRequest; import winstone.WinstoneResourceBundle; import winstone.WinstoneResponse; /** * Implements the main listener daemon thread. This is the class that gets * launched by the command line, and owns the server socket, etc. * * @author mailto: <a href="rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: Ajp13Listener.java,v 1.12 2006/03/24 17:24:22 rickknowles Exp $ */ public class Ajp13Listener implements Listener, Runnable { public final static WinstoneResourceBundle AJP_RESOURCES = new WinstoneResourceBundle("winstone.ajp13.LocalStrings"); private final static int LISTENER_TIMEOUT = 5000; // every 5s reset the listener socket private final static int DEFAULT_PORT = 8009; private final static int CONNECTION_TIMEOUT = 60000; private final static int BACKLOG_COUNT = 1000; private final static int KEEP_ALIVE_TIMEOUT = -1; // private final static int KEEP_ALIVE_SLEEP = 50; // private final static int KEEP_ALIVE_SLEEP_MAX = 500; private final static String TEMPORARY_URL_STASH = "winstone.ajp13.TemporaryURLAttribute"; private HostGroup hostGroup; private ObjectPool objectPool; private int listenPort; private boolean interrupted; private String listenAddress; /** * Constructor */ public Ajp13Listener(Map args, ObjectPool objectPool, HostGroup hostGroup) { // Load resources this.hostGroup = hostGroup; this.objectPool = objectPool; this.listenPort = Integer.parseInt(WebAppConfiguration.stringArg(args, "ajp13Port", "" + DEFAULT_PORT)); this.listenAddress = WebAppConfiguration.stringArg(args, "ajp13ListenAddress", null); } public boolean start() { if (this.listenPort < 0) { return false; } else { this.interrupted = false; Thread thread = new Thread(this, Launcher.RESOURCES.getString( "Listener.ThreadName", new String[] { "ajp13", "" + this.listenPort })); thread.setDaemon(true); thread.start(); return true; } } /** * The main run method. This handles the normal thread processing. */ public void run() { try { ServerSocket ss = this.listenAddress == null ? new ServerSocket( this.listenPort, BACKLOG_COUNT) : new ServerSocket( this.listenPort, BACKLOG_COUNT, InetAddress .getByName(this.listenAddress)); ss.setSoTimeout(LISTENER_TIMEOUT); Logger.log(Logger.INFO, AJP_RESOURCES, "Ajp13Listener.StartupOK", this.listenPort + ""); // Enter the main loop while (!interrupted) { // Get the listener Socket s = null; try { s = ss.accept(); } catch (java.io.InterruptedIOException err) { s = null; } // if we actually got a socket, process it. Otherwise go around // again if (s != null) this.objectPool.handleRequest(s, this); } // Close server socket ss.close(); } catch (Throwable err) { Logger.log(Logger.ERROR, AJP_RESOURCES, "Ajp13Listener.ShutdownError", err); } Logger.log(Logger.INFO, AJP_RESOURCES, "Ajp13Listener.ShutdownOK"); } /** * Interrupts the listener thread. This will trigger a listener shutdown * once the so timeout has passed. */ public void destroy() { this.interrupted = true; } /** * Called by the request handler thread, because it needs specific setup * code for this connection's protocol (ie construction of request/response * objects, in/out streams, etc). * * This implementation parses incoming AJP13 packets, and builds an * outputstream that is capable of writing back the response in AJP13 * packets. */ public void allocateRequestResponse(Socket socket, InputStream inSocket, OutputStream outSocket, RequestHandlerThread handler, boolean iAmFirst) throws SocketException, IOException { WinstoneRequest req = this.objectPool.getRequestFromPool(); WinstoneResponse rsp = this.objectPool.getResponseFromPool(); rsp.setRequest(req); req.setHostGroup(this.hostGroup); // rsp.updateContentTypeHeader("text/html"); if (iAmFirst || (KEEP_ALIVE_TIMEOUT == -1)) socket.setSoTimeout(CONNECTION_TIMEOUT); else socket.setSoTimeout(KEEP_ALIVE_TIMEOUT); Ajp13IncomingPacket headers = null; try { headers = new Ajp13IncomingPacket(inSocket, handler); } catch (InterruptedIOException err) { // keep alive timeout ? ignore if not first if (iAmFirst) { throw err; } else { deallocateRequestResponse(handler, req, rsp, null, null); return; } } finally { try {socket.setSoTimeout(CONNECTION_TIMEOUT);} catch (Throwable err) {} } if (headers.getPacketLength() > 0) { headers.parsePacket("8859_1"); parseSocketInfo(headers, req); req.parseHeaders(Arrays.asList(headers.getHeaders())); String servletURI = parseURILine(headers, req, rsp); req.setAttribute(TEMPORARY_URL_STASH, servletURI); // If content-length present and non-zero, download the other // packets WinstoneInputStream inData = null; int contentLength = req.getContentLength(); if (contentLength > 0) { byte bodyContent[] = new byte[contentLength]; int position = 0; while (position < contentLength) { outSocket.write(getBodyRequestPacket(Math.min(contentLength - position, 8184))); position = getBodyResponsePacket(inSocket, bodyContent, position); Logger.log(Logger.FULL_DEBUG, AJP_RESOURCES, "Ajp13Listener.ReadBodyProgress", new String[] { "" + position, "" + contentLength }); } inData = new WinstoneInputStream(bodyContent); inData.setContentLength(contentLength); } else inData = new WinstoneInputStream(new byte[0]); req.setInputStream(inData); // Build input/output streams, plus request/response WinstoneOutputStream outData = new Ajp13OutputStream(socket .getOutputStream(), "8859_1"); outData.setResponse(rsp); rsp.setOutputStream(outData); // Set the handler's member variables so it can execute the servlet handler.setRequest(req); handler.setResponse(rsp); handler.setInStream(inData); handler.setOutStream(outData); } } /** * Called by the request handler thread, because it needs specific shutdown * code for this connection's protocol (ie releasing input/output streams, * etc). */ public void deallocateRequestResponse(RequestHandlerThread handler, WinstoneRequest req, WinstoneResponse rsp, WinstoneInputStream inData, WinstoneOutputStream outData) throws IOException { handler.setInStream(null); handler.setOutStream(null); handler.setRequest(null); handler.setResponse(null); if (req != null) this.objectPool.releaseRequestToPool(req); if (rsp != null) this.objectPool.releaseResponseToPool(rsp); } /** * This is kind of a hack, since we have already parsed the uri to get the * input stream. Just pass back the request uri */ public String parseURI(RequestHandlerThread handler, WinstoneRequest req, WinstoneResponse rsp, WinstoneInputStream inData, Socket socket, boolean iAmFirst) throws IOException { String uri = (String) req.getAttribute(TEMPORARY_URL_STASH); req.removeAttribute(TEMPORARY_URL_STASH); return uri; } /** * Called by the request handler thread, because it needs specific shutdown * code for this connection's protocol if the keep-alive period expires (ie * closing sockets, etc). * * This implementation simply shuts down the socket and streams. */ public void releaseSocket(Socket socket, InputStream inSocket, OutputStream outSocket) throws IOException { // Logger.log(Logger.FULL_DEBUG, "Releasing socket: " + // Thread.currentThread().getName()); inSocket.close(); outSocket.close(); socket.close(); } /** * Extract the header details relating to socket stuff from the ajp13 header * packet */ private void parseSocketInfo(Ajp13IncomingPacket headers, WinstoneRequest req) { req.setServerPort(headers.getServerPort()); req.setRemoteIP(headers.getRemoteAddress()); req.setServerName(headers.getServerName()); req.setLocalPort(headers.getServerPort()); req.setLocalAddr(headers.getServerName()); req.setRemoteIP(headers.getRemoteAddress()); if ((headers.getRemoteHost() != null) && !headers.getRemoteHost().equals("")) req.setRemoteName(headers.getRemoteHost()); else req.setRemoteName(headers.getRemoteAddress()); req.setScheme(headers.isSSL() ? "https" : "http"); req.setIsSecure(headers.isSSL()); } /** * Extract the header details relating to protocol, uri, etc from the ajp13 * header packet */ private String parseURILine(Ajp13IncomingPacket headers, WinstoneRequest req, WinstoneResponse rsp) throws UnsupportedEncodingException { req.setMethod(headers.getMethod()); req.setProtocol(headers.getProtocol()); rsp.setProtocol(headers.getProtocol()); rsp.extractRequestKeepAliveHeader(req); // req.setServletPath(headers.getURI()); // req.setRequestURI(headers.getURI()); // Get query string if supplied for (Iterator i = headers.getAttributes().keySet().iterator(); i .hasNext();) { String attName = (String) i.next(); if (attName.equals("query_string")) { String qs = (String) headers.getAttributes().get("query_string"); req.setQueryString(qs); // req.getParameters().putAll(WinstoneRequest.extractParameters(qs, // req.getEncoding(), mainResources)); // req.setRequestURI(headers.getURI() + "?" + qs); } else if (attName.equals("ssl_cert")) { String certValue = (String) headers.getAttributes().get( "ssl_cert"); InputStream certStream = new ByteArrayInputStream(certValue .getBytes("8859_1")); X509Certificate certificateArray[] = new X509Certificate[1]; try { certificateArray[0] = (X509Certificate) CertificateFactory .getInstance("X.509").generateCertificate( certStream); } catch (CertificateException err) { Logger.log(Logger.DEBUG, AJP_RESOURCES, "Ajp13Listener.SkippingCert", certValue); } req.setAttribute("javax.servlet.request.X509Certificate", certificateArray); req.setIsSecure(true); } else if (attName.equals("ssl_cipher")) { String cipher = (String) headers.getAttributes().get( "ssl_cipher"); req.setAttribute("javax.servlet.request.cipher_suite", cipher); req.setAttribute("javax.servlet.request.key_size", getKeySize(cipher)); req.setIsSecure(true); } else if (attName.equals("ssl_session")) { req.setAttribute("javax.servlet.request.ssl_session", headers .getAttributes().get("ssl_session")); req.setIsSecure(true); } else Logger.log(Logger.DEBUG, AJP_RESOURCES, "Ajp13Listener.UnknownAttribute", new String[] { attName, "" + headers.getAttributes().get(attName) }); } return headers.getURI(); } private Integer getKeySize(String cipherSuite) { if (cipherSuite.indexOf("_WITH_NULL_") != -1) return new Integer(0); else if (cipherSuite.indexOf("_WITH_IDEA_CBC_") != -1) return new Integer(128); else if (cipherSuite.indexOf("_WITH_RC2_CBC_40_") != -1) return new Integer(40); else if (cipherSuite.indexOf("_WITH_RC4_40_") != -1) return new Integer(40); else if (cipherSuite.indexOf("_WITH_RC4_128_") != -1) return new Integer(128); else if (cipherSuite.indexOf("_WITH_DES40_CBC_") != -1) return new Integer(40); else if (cipherSuite.indexOf("_WITH_DES_CBC_") != -1) return new Integer(56); else if (cipherSuite.indexOf("_WITH_3DES_EDE_CBC_") != -1) return new Integer(168); else return null; } /** * Tries to wait for extra requests on the same socket. If any are found * before the timeout expires, it exits with a true, indicating a new * request is waiting. If the timeout expires, return a false, instructing * the handler thread to begin shutting down the socket and relase itself. */ public boolean processKeepAlive(WinstoneRequest request, WinstoneResponse response, InputStream inSocket) throws IOException, InterruptedException { return true; } /** * Build the packet needed for asking for a body chunk */ private byte[] getBodyRequestPacket(int desiredPacketLength) { byte getBodyRequestPacket[] = new byte[] { 0x41, 0x42, 0x00, 0x03, 0x06, 0x00, 0x00 }; Ajp13OutputStream.setIntBlock(desiredPacketLength, getBodyRequestPacket, 5); return getBodyRequestPacket; } /** * Process the server response to a get_body_chunk request. This loads the * packet from the stream, and unpacks it into the buffer at the right * place. */ private int getBodyResponsePacket(InputStream in, byte buffer[], int offset) throws IOException { // Get the incoming packet flag byte headerBuffer[] = new byte[4]; int headerBytesRead = in.read(headerBuffer); if (headerBytesRead != 4) throw new WinstoneException(AJP_RESOURCES .getString("Ajp13Listener.InvalidHeader")); else if ((headerBuffer[0] != 0x12) || (headerBuffer[1] != 0x34)) throw new WinstoneException(AJP_RESOURCES .getString("Ajp13Listener.InvalidHeader")); // Read in the whole packet int packetLength = ((headerBuffer[2] & 0xFF) << 8) + (headerBuffer[3] & 0xFF); if (packetLength == 0) return offset; // Look for packet length byte bodyLengthBuffer[] = new byte[2]; in.read(bodyLengthBuffer); int bodyLength = ((bodyLengthBuffer[0] & 0xFF) << 8) + (bodyLengthBuffer[1] & 0xFF); int packetBytesRead = in.read(buffer, offset, bodyLength); if (packetBytesRead < bodyLength) throw new WinstoneException(AJP_RESOURCES .getString("Ajp13Listener.ShortPacket")); else return packetBytesRead + offset; } // // /** // * Useful method for dumping out the contents of a packet in hex form // */ // public static void packetDump(byte packetBytes[], int packetLength) { // String dump = ""; // for (int n = 0; n < packetLength; n+=16) { // String line = Integer.toHexString((n >> 4) & 0xF) + "0:"; // for (int j = 0; j < Math.min(packetLength - n, 16); j++) // line = line + " " + ((packetBytes[n + j] & 0xFF) < 16 ? "0" : "") + // Integer.toHexString(packetBytes[n + j] & 0xFF); // // line = line + " "; // for (int j = 0; j < Math.min(packetLength - n, 16); j++) { // byte me = (byte) (packetBytes[n + j] & 0xFF); // line = line + (((me > 32) && (me < 123)) ? (char) me : '.'); // } // dump = dump + line + "\r\n"; // } // System.out.println(dump); // } }