/* * 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; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Implements the main listener daemon thread. This is the class that gets * launched by the command line, and owns the server socket, etc. Note that this * class is also used as the base class for the HTTPS listener. * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: HttpListener.java,v 1.15 2007/05/01 04:39:49 rickknowles Exp $ */ public class HttpListener implements Listener, Runnable { protected static int LISTENER_TIMEOUT = 5000; // every 5s reset the // listener socket protected static int CONNECTION_TIMEOUT = 60000; protected static int BACKLOG_COUNT = 5000; protected static boolean DEFAULT_HNL = false; protected static int KEEP_ALIVE_TIMEOUT = 10000; protected static int KEEP_ALIVE_SLEEP = 20; protected static int KEEP_ALIVE_SLEEP_MAX = 500; protected HostGroup hostGroup; protected ObjectPool objectPool; protected boolean doHostnameLookups; protected int listenPort; protected String listenAddress; protected boolean interrupted; protected HttpListener() { } /** * Constructor */ public HttpListener(Map args, ObjectPool objectPool, HostGroup hostGroup) throws IOException { // Load resources this.hostGroup = hostGroup; this.objectPool = objectPool; this.listenPort = Integer.parseInt(WebAppConfiguration.stringArg(args, getConnectorName() + "Port", "" + getDefaultPort())); this.listenAddress = WebAppConfiguration.stringArg(args, getConnectorName() + "ListenAddress", null); this.doHostnameLookups = WebAppConfiguration.booleanArg(args, getConnectorName() + "DoHostnameLookups", DEFAULT_HNL); } 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[] { getConnectorName(), "" + this.listenPort })); thread.setDaemon(true); thread.start(); return true; } } /** * The default port to use - this is just so that we can override for the * SSL connector. */ protected int getDefaultPort() { return 8080; } /** * The name to use when getting properties - this is just so that we can * override for the SSL connector. */ protected String getConnectorName() { return getConnectorScheme(); } protected String getConnectorScheme() { return "http"; } /** * Gets a server socket - this is mostly for the purpose of allowing an * override in the SSL connector. */ protected ServerSocket getServerSocket() throws IOException { ServerSocket ss = this.listenAddress == null ? new ServerSocket( this.listenPort, BACKLOG_COUNT) : new ServerSocket( this.listenPort, BACKLOG_COUNT, InetAddress .getByName(this.listenAddress)); return ss; } /** * The main run method. This continually listens for incoming connections, * and allocates any that it finds to a request handler thread, before going * back to listen again. */ public void run() { try { ServerSocket ss = getServerSocket(); ss.setSoTimeout(LISTENER_TIMEOUT); Logger.log(Logger.INFO, Launcher.RESOURCES, "HttpListener.StartupOK", new String[] { getConnectorName().toUpperCase(), 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, Launcher.RESOURCES, "HttpListener.ShutdownError", getConnectorName().toUpperCase(), err); } Logger.log(Logger.INFO, Launcher.RESOURCES, "HttpListener.ShutdownOK", getConnectorName().toUpperCase()); } /** * 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 { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "HttpListener.AllocatingRequest", Thread.currentThread() .getName()); socket.setSoTimeout(CONNECTION_TIMEOUT); // Build input/output streams, plus request/response WinstoneInputStream inData = new WinstoneInputStream(inSocket); WinstoneOutputStream outData = new WinstoneOutputStream(outSocket, false); WinstoneRequest req = this.objectPool.getRequestFromPool(); WinstoneResponse rsp = this.objectPool.getResponseFromPool(); outData.setResponse(rsp); req.setInputStream(inData); rsp.setOutputStream(outData); rsp.setRequest(req); // rsp.updateContentTypeHeader("text/html"); req.setHostGroup(this.hostGroup); // Set the handler's member variables so it can execute the servlet handler.setRequest(req); handler.setResponse(rsp); handler.setInStream(inData); handler.setOutStream(outData); // If using this listener, we must set the server header now, because it // must be the first header. Ajp13 listener can defer to the Apache Server // header rsp.setHeader("Server", Launcher.RESOURCES.getString("ServerVersion")); } /** * 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); } public String parseURI(RequestHandlerThread handler, WinstoneRequest req, WinstoneResponse rsp, WinstoneInputStream inData, Socket socket, boolean iAmFirst) throws IOException { parseSocketInfo(socket, req); // Read the header line (because this is the first line of the request, // apply keep-alive timeouts to it if we are not the first request) if (!iAmFirst) { socket.setSoTimeout(KEEP_ALIVE_TIMEOUT); } byte uriBuffer[] = null; try { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "HttpListener.WaitingForURILine"); uriBuffer = inData.readLine(); } catch (InterruptedIOException err) { // keep alive timeout ? ignore if not first if (iAmFirst) { throw err; } else { return null; } } finally { try {socket.setSoTimeout(CONNECTION_TIMEOUT);} catch (Throwable err) {} } handler.setRequestStartTime(); // Get header data (eg protocol, method, uri, headers, etc) String uriLine = new String(uriBuffer); if (uriLine.trim().equals("")) throw new SocketException("Empty URI Line"); String servletURI = parseURILine(uriLine, req, rsp); parseHeaders(req, inData); rsp.extractRequestKeepAliveHeader(req); int contentLength = req.getContentLength(); if (contentLength != -1) inData.setContentLength(contentLength); return servletURI; } /** * 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(); } protected void parseSocketInfo(Socket socket, WinstoneRequest req) throws IOException { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "HttpListener.ParsingSocketInfo"); req.setScheme(getConnectorScheme()); req.setServerPort(socket.getLocalPort()); req.setLocalPort(socket.getLocalPort()); req.setLocalAddr(socket.getLocalAddress().getHostAddress()); req.setRemoteIP(socket.getInetAddress().getHostAddress()); req.setRemotePort(socket.getPort()); if (this.doHostnameLookups) { req.setServerName(socket.getLocalAddress().getHostName()); req.setRemoteName(socket.getInetAddress().getHostName()); req.setLocalName(socket.getLocalAddress().getHostName()); } else { req.setServerName(socket.getLocalAddress().getHostAddress()); req.setRemoteName(socket.getInetAddress().getHostAddress()); req.setLocalName(socket.getLocalAddress().getHostAddress()); } } /** * 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 protocol does not support keep-alives, or the * request instructed us to close the connection, or 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 { // Try keep alive if allowed boolean continueFlag = !response.closeAfterRequest(); return continueFlag; } /** * Processes the uri line into it's component parts, determining protocol, * method and uri */ private String parseURILine(String uriLine, WinstoneRequest req, WinstoneResponse rsp) { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "HttpListener.UriLine", uriLine.trim()); // Method int spacePos = uriLine.indexOf(' '); if (spacePos == -1) throw new WinstoneException(Launcher.RESOURCES.getString( "HttpListener.ErrorUriLine", uriLine)); String method = uriLine.substring(0, spacePos).toUpperCase(); String fullURI = null; // URI String remainder = uriLine.substring(spacePos + 1); spacePos = remainder.indexOf(' '); if (spacePos == -1) { fullURI = trimHostName(remainder.trim()); req.setProtocol("HTTP/0.9"); rsp.setProtocol("HTTP/0.9"); } else { fullURI = trimHostName(remainder.substring(0, spacePos).trim()); String protocol = remainder.substring(spacePos + 1).trim().toUpperCase(); req.setProtocol(protocol); rsp.setProtocol(protocol); } req.setMethod(method); // req.setRequestURI(fullURI); return fullURI; } private String trimHostName(String input) { if (input == null) return null; else if (input.startsWith("/")) return input; int hostStart = input.indexOf("://"); if (hostStart == -1) return input; String hostName = input.substring(hostStart + 3); int pathStart = hostName.indexOf('/'); if (pathStart == -1) return "/"; else return hostName.substring(pathStart); } /** * Parse the incoming stream into a list of headers (stopping at the first * blank line), then call the parseHeaders(req, list) method on that list. */ public void parseHeaders(WinstoneRequest req, WinstoneInputStream inData) throws IOException { List headerList = new ArrayList(); if (!req.getProtocol().startsWith("HTTP/0")) { // Loop to get headers byte headerBuffer[] = inData.readLine(); String headerLine = new String(headerBuffer); while (headerLine.trim().length() > 0) { if (headerLine.indexOf(':') != -1) { headerList.add(headerLine.trim()); Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "HttpListener.Header", headerLine.trim()); } headerBuffer = inData.readLine(); headerLine = new String(headerBuffer); } } // If no headers available, parse an empty list req.parseHeaders(headerList); } }