/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.stetho.server.http; import android.net.LocalSocket; import android.net.Uri; import android.support.annotation.Nullable; import com.facebook.stetho.server.LeakyBufferedInputStream; import com.facebook.stetho.server.SocketLike; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; /** * Somewhat crude but very fast HTTP server designed exclusively to handle the * Chrome DevTools protocol, though sufficiently general to do other very basic things. * Performance is imperative here as Chrome aggressively polls Stetho asking for * meta data when the discovery window is open in Chrome. */ public class LightHttpServer { private static final String TAG = "LightHttpServer"; private final HandlerRegistry mHandlerRegistry; public LightHttpServer(HandlerRegistry handlerRegistry) { mHandlerRegistry = handlerRegistry; } public void serve(SocketLike socket) throws IOException { LeakyBufferedInputStream input = new LeakyBufferedInputStream(socket.getInput(), 1024); OutputStream output = socket.getOutput(); HttpMessageReader reader = new HttpMessageReader(input); HttpMessageWriter writer = new HttpMessageWriter(new BufferedOutputStream(output)); SocketLike anotherSocketLike = new SocketLike(socket, input); LightHttpRequest scratchRequest = new LightHttpRequest(); LightHttpResponse scratchResponse = new LightHttpResponse(); LightHttpRequest request; // This loops assumes we are always using keep-alive connections. If we're wrong, we // expect the client to just close the connection. while ((request = readRequestMessage(scratchRequest, reader)) != null) { final LightHttpResponse response = scratchResponse; response.reset(); // Note, if we're upgrading to websockets, this will block for the lifetime of the // websocket session... boolean keepGoing = dispatchToHandler(anotherSocketLike, request, response); if (!keepGoing) { // Orderly shutdown, ignore response and break the loop. break; } writeFullResponse(response, writer, output); } } private boolean dispatchToHandler( SocketLike socketLike, LightHttpRequest request, LightHttpResponse response) throws IOException { HttpHandler handler = mHandlerRegistry.lookup(request.uri.getPath()); if (handler == null) { response.code = HttpStatus.HTTP_NOT_FOUND; response.reasonPhrase = "Not found"; response.body = LightHttpBody.create("No handler found\n", "text/plain"); return true; } else { try { return handler.handleRequest(socketLike, request, response); } catch (RuntimeException e) { response.code = HttpStatus.HTTP_INTERNAL_SERVER_ERROR; response.reasonPhrase = "Internal Server Error"; StringWriter stack = new StringWriter(); PrintWriter stackWriter = new PrintWriter(stack); try { e.printStackTrace(stackWriter); } finally { stackWriter.close(); } response.body = LightHttpBody.create(stack.toString(), "text/plain"); return true; } } } @Nullable private static LightHttpRequest readRequestMessage( LightHttpRequest request, HttpMessageReader reader) throws IOException { request.reset(); String requestLine = reader.readLine(); if (requestLine == null) { return null; } // Zero tolerance on URI encoding, that URI better not have a space in it... String[] requestParts = requestLine.split(" ", 3); if (requestParts.length != 3) { throw new IOException("Invalid request line: " + requestLine); } request.method = requestParts[0]; request.uri = Uri.parse(requestParts[1]); request.protocol = requestParts[2]; readHeaders(request, reader); return request; } private static void readHeaders( LightHttpMessage message, HttpMessageReader reader) throws IOException { String headerLine; while (true) { headerLine = reader.readLine(); if (headerLine == null) { throw new EOFException(); } else if ("".equals(headerLine)) { break; } else { String[] headerParts = headerLine.split(": ", 2); if (headerParts.length != 2) { throw new IOException("Malformed header: " + headerLine); } String name = headerParts[0]; String value = headerParts[1]; message.headerNames.add(name); message.headerValues.add(value); } } } private static void writeFullResponse( LightHttpResponse response, HttpMessageWriter writer, OutputStream output) throws IOException { response.prepare(); writeResponseMessage(response, writer); if (response.body != null) { response.body.writeTo(output); } } public static void writeResponseMessage(LightHttpResponse response, HttpMessageWriter writer) throws IOException { writer.writeLine("HTTP/1.1 " + response.code + " " + response.reasonPhrase); for (int i = 0, N = response.headerNames.size(); i < N; i++) { String name = response.headerNames.get(i); String value = response.headerValues.get(i); writer.writeLine(name + ": " + value); } writer.writeLine(); writer.flush(); } /** * Efficient, unbuffered variation of {@link InputStreamReader} which assumes the input is * always ASCII. This is especially useful when you are certain that the client and server * are both mechanized and will not contain non-ASCII characters in the control messages upon * which this reader is applied. */ private static class HttpMessageReader { private final BufferedInputStream mIn; private final StringBuilder mBuffer = new StringBuilder(); private final NewLineDetector mNewLineDetector = new NewLineDetector(); public HttpMessageReader(BufferedInputStream in) { mIn = in; } @Nullable public String readLine() throws IOException { while (true) { int b = mIn.read(); if (b < 0) { return null; } char c = (char)b; mNewLineDetector.accept(c); switch (mNewLineDetector.state()) { case NewLineDetector.STATE_ON_CRLF: String result = mBuffer.toString(); mBuffer.setLength(0); return result; case NewLineDetector.STATE_ON_CR: break; case NewLineDetector.STATE_ON_OTHER: mBuffer.append(c); break; } } } private static class NewLineDetector { private static final int STATE_ON_OTHER = 1; private static final int STATE_ON_CR = 2; private static final int STATE_ON_CRLF = 3; private int state = STATE_ON_OTHER; public void accept(char c) { switch (state) { case STATE_ON_OTHER: if (c == '\r') { state = STATE_ON_CR; } break; case STATE_ON_CR: if (c == '\n') { state = STATE_ON_CRLF; } else { state = STATE_ON_OTHER; } break; case STATE_ON_CRLF: if (c == '\r') { state = STATE_ON_CR; } else { state = STATE_ON_OTHER; } break; default: throw new IllegalArgumentException("Unknown state: " + state); } } public int state() { return state; } } } /** * Similar in spirit to {@link HttpMessageReader} which assumes ASCII for all messages as * a performance optimization. Caller is responsible for flushing the writer. * <p /> * Exposed publicly as a hack to support WebSocket upgrade. */ public static class HttpMessageWriter { private final BufferedOutputStream mOut; private static final byte[] CRLF = "\r\n".getBytes(); public HttpMessageWriter(BufferedOutputStream out) { mOut = out; } public void writeLine(String line) throws IOException { for (int i = 0, N = line.length(); i < N; i++) { char c = line.charAt(i); mOut.write((int)c); } mOut.write(CRLF); } public void writeLine() throws IOException { mOut.write(CRLF); } public void flush() throws IOException { mOut.flush(); } } }