package org.dynmap.web; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.StringWriter; import java.net.Socket; import java.net.URLDecoder; import java.util.Map.Entry; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.dynmap.Log; import org.dynmap.debug.Debug; public class HttpServerConnection extends Thread { protected static final Logger log = Logger.getLogger("Minecraft"); protected static final String LOG_PREFIX = "[dynmap] "; private static Pattern requestHeaderLine = Pattern.compile("^(\\S+)\\s+(\\S+)\\s+HTTP/(.+)$"); private static Pattern requestHeaderField = Pattern.compile("^([^:]+):\\s*(.+)$"); private Socket socket; private HttpServer server; private PrintStream printOut; private StringWriter sw = new StringWriter(); private Matcher requestHeaderLineMatcher; private Matcher requestHeaderFieldMatcher; public HttpServerConnection(Socket socket, HttpServer server) { this.socket = socket; this.server = server; } private final static void readLine(InputStream in, StringWriter sw) throws IOException { int readc; while((readc = in.read()) > 0) { char c = (char)readc; if (c == '\n') break; else if (c != '\r') sw.append(c); } } private final String readLine(InputStream in) throws IOException { readLine(in, sw); String r = sw.toString(); sw.getBuffer().setLength(0); return r; } private final boolean readRequestHeader(InputStream in, HttpRequest request) throws IOException { String statusLine = readLine(in); if (statusLine == null) return false; if (requestHeaderLineMatcher == null) { requestHeaderLineMatcher = requestHeaderLine.matcher(statusLine); } else { requestHeaderLineMatcher.reset(statusLine); } Matcher m = requestHeaderLineMatcher; if (!m.matches()) return false; request.method = m.group(1); request.path = m.group(2); request.version = m.group(3); String line; while (!(line = readLine(in)).equals("")) { if (requestHeaderFieldMatcher == null) { requestHeaderFieldMatcher = requestHeaderField.matcher(line); } else { requestHeaderFieldMatcher.reset(line); } m = requestHeaderFieldMatcher; // Warning: unknown lines are ignored. if (m.matches()) { String fieldName = m.group(1); String fieldValue = m.group(2); // TODO: Does not support duplicate field-names. request.fields.put(fieldName, fieldValue); } } return true; } public static final void writeResponseHeader(PrintStream out, HttpResponse response) throws IOException { out.append("HTTP/"); out.append(response.version); out.append(" "); out.append(String.valueOf(response.status.getCode())); out.append(" "); out.append(response.status.getText()); out.append("\r\n"); for (Entry field : response.fields.entrySet()) { out.append(field.getKey()); out.append(": "); out.append(field.getValue()); out.append("\r\n"); } out.append("\r\n"); out.flush(); } public final void writeResponseHeader(HttpResponse response) throws IOException { writeResponseHeader(printOut, response); } public void run() { try { if (socket == null) return; socket.setSoTimeout(5000); InputStream in = socket.getInputStream(); BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream(), 40960); printOut = new PrintStream(out, false); while (true) { HttpRequest request = new HttpRequest(); if (!readRequestHeader(in, request)) { socket.close(); return; } long bound = -1; BoundInputStream boundBody = null; { String contentLengthStr = request.fields.get(HttpField.ContentLength); if (contentLengthStr != null) { try { bound = Long.parseLong(contentLengthStr); } catch (NumberFormatException e) { } if (bound >= 0) { request.body = boundBody = new BoundInputStream(in, bound); } else { request.body = in; } } } // TODO: Optimize HttpHandler-finding by using a real path-aware tree. HttpHandler handler = null; String relativePath = null; for (Entry entry : server.handlers.entrySet()) { String key = entry.getKey(); boolean directoryHandler = key.endsWith("/"); if (directoryHandler && request.path.startsWith(entry.getKey()) || !directoryHandler && request.path.equals(entry.getKey())) { relativePath = request.path.substring(entry.getKey().length()); relativePath = URLDecoder.decode(relativePath,"utf-8"); handler = entry.getValue(); break; } /* Wildcard handler for non-directory matches */ else if(key.endsWith("*") && request.path.startsWith(key.substring(0, key.length()-1))) { relativePath = request.path.substring(entry.getKey().length()); relativePath = request.path.substring(entry.getKey().length()-1); relativePath = URLDecoder.decode(relativePath,"utf-8"); handler = entry.getValue(); break; } } if (handler == null) { socket.close(); return; } HttpResponse response = new HttpResponse(this, out); try { handler.handle(relativePath, request, response); } catch (IOException e) { throw e; } catch (Exception e) { Log.severe("HttpHandler '" + handler + "' has thown an exception", e); if (socket != null) { out.flush(); socket.close(); } return; } if (bound > 0 && boundBody.skip(bound) < bound) { Debug.debug("Incoming stream was only read partially by handler '" + handler + "'."); //socket.close(); //return; } boolean isKeepalive = !"close".equals(request.fields.get(HttpField.Connection)) && !"close".equals(response.fields.get(HttpField.Connection)); String contentLength = response.fields.get("Content-Length"); if (isKeepalive && contentLength == null) { // A handler has been a bad boy, but we're here to fix it. response.fields.put("Content-Length", "0"); OutputStream responseBody = response.getBody(); // The HttpHandler has already send the headers and written to the body without setting the Content-Length. if (responseBody == null) { Debug.debug("Response was given without Content-Length by '" + handler + "' for path '" + request.path + "'."); out.flush(); socket.close(); return; } } if (!isKeepalive) { out.flush(); socket.close(); return; } out.flush(); } } catch (IOException e) { if (socket != null) { try { socket.close(); } catch (IOException ex) { } } return; } catch (Exception e) { if (socket != null) { try { socket.close(); } catch (IOException ex) { } } Log.severe("Exception while handling request: ", e); e.printStackTrace(); return; } } }