package org.couchbase.mock.httpio; import org.apache.http.*; import org.apache.http.impl.DefaultBHttpServerConnectionFactory; import org.apache.http.protocol.*; import org.apache.http.util.VersionInfo; import org.couchbase.mock.Info; import java.io.IOException; import java.net.*; import java.nio.channels.ServerSocketChannel; import java.util.HashSet; import java.util.Set; public class HttpServer extends Thread { /** * Subclass of HttpService which adds some additional hooks to all responses */ static class MyHttpService extends HttpService { MyHttpService(HttpProcessor proc, UriHttpRequestHandlerMapper registry) { super(proc, registry); } @Override protected void doService(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { response.addHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate"); // Disable caching // Normalize the URI super.doService(request, response, context); } } /** * Subclass of UriHttpRequestHandlerMapper which normalizes a URI and matches only its path. * This is because we ignore the host part (we don't use the {@code Host} header) */ static class MyRequestHandlerMapper extends UriHttpRequestHandlerMapper { @Override protected String getRequestPath(final HttpRequest request) { String s = request.getRequestLine().getUri(); try { URI uri = new URI(s); return uri.getPath(); } catch (URISyntaxException ex) { return s; } catch (IllegalArgumentException ex) { return s; } } } private volatile boolean shouldRun = true; private final DefaultBHttpServerConnectionFactory connectionFactory; private final HttpService httpService; private final UriHttpRequestHandlerMapper registry; private final Set<Worker> allWorkers = new HashSet<Worker>(); private static final String serverString = String.format("CouchbaseMock/%s (mcd; views) httpcomponents/%s", Info.getVersion(), VersionInfo.loadVersionInfo("org.apache.http", null).getRelease()); private ServerSocketChannel listener; final public static String CX_SOCKET = "couchbase.mock.http.socket"; final public static String CX_AUTH = "couchbase.mock.http.auth"; /** * Creates a new server. To make the server respond to requests, invoke * the {@link #bind(java.net.InetSocketAddress)} method to make it use a socket, * and then invoke the {@link #start()} method to start it up in the background. * * Use {@link #register(String, org.apache.http.protocol.HttpRequestHandler)} to add * handlers which respond to various URL paths */ public HttpServer() { this.connectionFactory = new DefaultBHttpServerConnectionFactory(); this.registry = new MyRequestHandlerMapper(); HttpProcessor httpProcessor = HttpProcessorBuilder.create() .add(new ResponseServer(serverString)) .add(new ResponseContent()) .add(new ResponseConnControl()) .build(); this.httpService = new MyHttpService(httpProcessor, registry); // Register the unknown handler register("*", new HttpRequestHandler() { @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { response.setStatusCode(HttpStatus.SC_NOT_FOUND); } }); } /** * Set the server's listening address * @param address The address the server should listen on * @throws IOException if a new socket could not be created * @see {@link #bind(java.nio.channels.ServerSocketChannel)} */ public void bind(InetSocketAddress address) throws IOException { if (listener != null) { listener.close(); listener = null; } listener = ServerSocketChannel.open(); listener.socket().bind(address); } /** * Set the server's listening socket. * @param newSock An existing listening socket. * @see {@link #bind(java.net.InetSocketAddress)} */ public void bind(ServerSocketChannel newSock) { listener = newSock; } /** * Register a path with a handler * @param pattern The path to register * @param handler The handler to handle the path * @see {@link org.apache.http.protocol.UriHttpRequestHandlerMapper} */ public void register(String pattern, HttpRequestHandler handler) { registry.register(pattern, handler); registry.register(pattern + "/", handler); } /** * Unregister a given path. Further requests to paths matching the specified * pattern will result in a 404 being delivered to the client * @param pattern The pattern to unregister. Must have previously been registered * via {@link #register(String, org.apache.http.protocol.HttpRequestHandler)} */ public void unregister(String pattern) { registry.unregister(pattern); registry.unregister(pattern + "/"); } class Worker extends Thread { final HttpServerConnection htConn; final Socket rawSocket; private volatile boolean closeRequested = false; Worker(HttpServerConnection htConn, Socket rawSocket) { this.htConn = htConn; this.rawSocket = rawSocket; setName("Mock Http Worker: " + rawSocket.getRemoteSocketAddress()); } void stopSocket() { closeRequested = true; try { this.rawSocket.close(); } catch (IOException ex) { // } } private void bail() { this.stopSocket(); } public void doReadLoop() { HttpContext context = new BasicHttpContext(); context.setAttribute(CX_SOCKET, rawSocket); while (!Thread.interrupted() && this.htConn.isOpen() && HttpServer.this.shouldRun) { // Clear the context from any auth settings; since this is done // anew on each connection.. context.removeAttribute(CX_AUTH); try { HttpServer.this.httpService.handleRequest(htConn, context); } catch (ConnectionClosedException ex_closed) { break; } catch (IOException ex) { if (!closeRequested) { ex.printStackTrace(); } break; } catch (HttpException ex) { ex.printStackTrace(); break; } catch (ResponseHandledException ex) { break; } } bail(); } @Override public void run() { try { doReadLoop(); } finally { synchronized (HttpServer.this.allWorkers) { HttpServer.this.allWorkers.remove(this); } bail(); } } } @Override public void run() { setName("Mock HTTP Listener: "+listener.socket().getInetAddress()); while (shouldRun) { Socket incoming; try { incoming = listener.accept().socket(); HttpServerConnection conn = connectionFactory.createConnection(incoming); Worker worker = new Worker(conn, incoming); synchronized (allWorkers) { allWorkers.add(worker); } worker.start(); } catch (IOException ex) { if (shouldRun) { ex.printStackTrace(); } } } } /** * Shut down the HTTP server and all its workers, and close the listener socket. */ public void stopServer() { shouldRun = false; try { listener.close(); } catch (IOException ex) { // Don't care } while (true) { synchronized (allWorkers) { if (allWorkers.isEmpty()) { break; } for (Worker w : allWorkers) { w.stopSocket(); w.interrupt(); } } } try { listener.close(); } catch (IOException ex) { ex.printStackTrace(); } } }