diff --git a/src/main/java/HTTPServer.java b/src/main/java/HTTPServer.java deleted file mode 100644 index 9dc4ae5..0000000 --- a/src/main/java/HTTPServer.java +++ /dev/null @@ -1,3103 +0,0 @@ -/* - * Copyright © 2005-2018 Amichai Rothman - * - * This file is part of JLHTTP - the Java Lightweight HTTP Server. - * - * JLHTTP is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * JLHTTP is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with JLHTTP. If not, see . - * - * For additional info see http://www.freeutils.net/source/jlhttp/ - */ - -import java.io.*; -import java.lang.annotation.*; -import java.lang.reflect.*; -import java.net.*; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.*; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.GZIPOutputStream; -import javax.net.ServerSocketFactory; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.SSLSocket; - -/** - * The {@code HTTPServer} class implements a light-weight HTTP server. - *

- * This server implements all functionality required by RFC 2616 ("Hypertext - * Transfer Protocol -- HTTP/1.1"), as well as some of the optional - * functionality (this is termed "conditionally compliant" in the RFC). - * In fact, a couple of bugs in the RFC itself were discovered - * (and fixed) during the development of this server. - *

- * Feature Overview - *

- *

- * Use Cases - *

- * Being a lightweight, standalone, easily embeddable and tiny-footprint - * server, it is well-suited for - *

- *

- * Implementation Notes - *

- * The design and implementation of this server attempt to balance correctness, - * compliance, readability, size, features, extensibility and performance, - * and often prioritize them in this order, but some trade-offs must be made. - *

- * This server is multithreaded in its support for multiple concurrent HTTP - * connections, however most of its constituent classes are not thread-safe and - * require external synchronization if accessed by multiple threads concurrently. - *

- * Source Structure and Documentation - *

- * This server is intentionally written as a single source file, in order to make - * it as easy as possible to integrate into any existing project - by simply adding - * this single file to the project sources. It does, however, aim to maintain a - * structured and flexible design. There are no external package dependencies. - *

- * This file contains extensive documentation of its classes and methods, as - * well as implementation details and references to specific RFC sections - * which clarify the logic behind the code. It is recommended that anyone - * attempting to modify the protocol-level functionality become acquainted with - * the RFC, in order to make sure that protocol compliance is not broken. - *

- * Getting Started - *

- * For an example and a good starting point for learning how to use the API, - * see the {@link #main main} method at the bottom of the file, and follow - * the code into the API from there. Alternatively, you can just browse through - * the classes and utility methods and read their documentation and code. - * - * @author Amichai Rothman - * @since 2008-07-24 - */ -public class HTTPServer { - - /** - * The SimpleDateFormat-compatible formats of dates which must be supported. - * Note that all generated date fields must be in the RFC 1123 format only, - * while the others are supported by recipients for backwards-compatibility. - */ - public static final String[] DATE_PATTERNS = { - "EEE, dd MMM yyyy HH:mm:ss z", // RFC 822, updated by RFC 1123 - "EEEE, dd-MMM-yy HH:mm:ss z", // RFC 850, obsoleted by RFC 1036 - "EEE MMM d HH:mm:ss yyyy" // ANSI C's asctime() format - }; - - /** A GMT (UTC) timezone instance. */ - protected static final TimeZone GMT = TimeZone.getTimeZone("GMT"); - - /** Date format strings. */ - protected static final char[] - DAYS = "Sun Mon Tue Wed Thu Fri Sat".toCharArray(), - MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".toCharArray(); - - /** A convenience array containing the carriage-return and line feed chars. */ - public static final byte[] CRLF = { 0x0d, 0x0a }; - - /** The HTTP status description strings. */ - protected static final String[] statuses = new String[600]; - - static { - // initialize status descriptions lookup table - Arrays.fill(statuses, "Unknown Status"); - statuses[100] = "Continue"; - statuses[200] = "OK"; - statuses[204] = "No Content"; - statuses[206] = "Partial Content"; - statuses[301] = "Moved Permanently"; - statuses[302] = "Found"; - statuses[304] = "Not Modified"; - statuses[307] = "Temporary Redirect"; - statuses[400] = "Bad Request"; - statuses[401] = "Unauthorized"; - statuses[403] = "Forbidden"; - statuses[404] = "Not Found"; - statuses[405] = "Method Not Allowed"; - statuses[408] = "Request Timeout"; - statuses[412] = "Precondition Failed"; - statuses[413] = "Request Entity Too Large"; - statuses[414] = "Request-URI Too Large"; - statuses[416] = "Requested Range Not Satisfiable"; - statuses[417] = "Expectation Failed"; - statuses[500] = "Internal Server Error"; - statuses[501] = "Not Implemented"; - statuses[502] = "Bad Gateway"; - statuses[503] = "Service Unavailable"; - statuses[504] = "Gateway Time-out"; - } - - /** - * A mapping of path suffixes (e.g. file extensions) to their - * corresponding MIME types. - */ - protected static final Map contentTypes = - new ConcurrentHashMap(); - - static { - // add some default common content types - // see http://www.iana.org/assignments/media-types/ for full list - addContentType("application/font-woff", "woff"); - addContentType("application/font-woff2", "woff2"); - addContentType("application/java-archive", "jar"); - addContentType("application/javascript", "js"); - addContentType("application/json", "json"); - addContentType("application/octet-stream", "exe"); - addContentType("application/pdf", "pdf"); - addContentType("application/x-7z-compressed", "7z"); - addContentType("application/x-compressed", "tgz"); - addContentType("application/x-gzip", "gz"); - addContentType("application/x-tar", "tar"); - addContentType("application/xhtml+xml", "xhtml"); - addContentType("application/zip", "zip"); - addContentType("audio/mpeg", "mp3"); - addContentType("image/gif", "gif"); - addContentType("image/jpeg", "jpg", "jpeg"); - addContentType("image/png", "png"); - addContentType("image/svg+xml", "svg"); - addContentType("image/x-icon", "ico"); - addContentType("text/css", "css"); - addContentType("text/csv", "csv"); - addContentType("text/html; charset=utf-8", "htm", "html"); - addContentType("text/plain", "txt", "text", "log"); - addContentType("text/xml", "xml"); - } - - /** The MIME types that can be compressed (prefix/suffix wildcards allowed). */ - protected static String[] compressibleContentTypes = - { "text/*", "*/javascript", "*icon", "*+xml", "*/json" }; - - /** - * The {@code LimitedInputStream} provides access to a limited number - * of consecutive bytes from the underlying InputStream, starting at its - * current position. If this limit is reached, it behaves as though the end - * of stream has been reached (although the underlying stream remains open - * and may contain additional data). - */ - public static class LimitedInputStream extends FilterInputStream { - - protected long limit; // decremented when read, until it reaches zero - protected boolean prematureEndException; - - /** - * Constructs a LimitedInputStream with the given underlying - * input stream and limit. - * - * @param in the underlying input stream - * @param limit the maximum number of bytes that may be consumed from - * the underlying stream before this stream ends. If zero or - * negative, this stream will be at its end from initialization. - * @param prematureEndException specifies the stream's behavior when - * the underlying stream end is reached before the limit is - * reached: if true, an exception is thrown, otherwise this - * stream reaches its end as well (i.e. read() returns -1) - * @throws NullPointerException if the given stream is null - */ - public LimitedInputStream(InputStream in, long limit, boolean prematureEndException) { - super(in); - if (in == null) - throw new NullPointerException("input stream is null"); - this.limit = limit < 0 ? 0 : limit; - this.prematureEndException = prematureEndException; - } - - @Override - public int read() throws IOException { - int res = limit == 0 ? -1 : in.read(); - if (res < 0 && limit > 0 && prematureEndException) - throw new IOException("unexpected end of stream"); - limit = res < 0 ? 0 : limit - 1; - return res; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int res = limit == 0 ? -1 : in.read(b, off, len > limit ? (int)limit : len); - if (res < 0 && limit > 0 && prematureEndException) - throw new IOException("unexpected end of stream"); - limit = res < 0 ? 0 : limit - res; - return res; - } - - @Override - public long skip(long len) throws IOException { - long res = in.skip(len > limit ? limit : len); - limit -= res; - return res; - } - - @Override - public int available() throws IOException { - int res = in.available(); - return res > limit ? (int)limit : res; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() { - limit = 0; // end this stream, but don't close the underlying stream - } - } - - /** - * The {@code ChunkedInputStream} decodes an InputStream whose data has the - * "chunked" transfer encoding applied to it, providing the underlying data. - */ - public static class ChunkedInputStream extends LimitedInputStream { - - protected Headers headers; - protected boolean initialized; - - /** - * Constructs a ChunkedInputStream with the given underlying stream, and - * a headers container to which the stream's trailing headers will be - * added. - * - * @param in the underlying "chunked"-encoded input stream - * @param headers the headers container to which the stream's trailing - * headers will be added, or null if they are to be discarded - * @throws NullPointerException if the given stream is null - */ - public ChunkedInputStream(InputStream in, Headers headers) { - super(in, 0, true); - this.headers = headers; - } - - @Override - public int read() throws IOException { - return limit <= 0 && initChunk() < 0 ? -1 : super.read(); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return limit <= 0 && initChunk() < 0 ? -1 : super.read(b, off, len); - } - - /** - * Initializes the next chunk. If the previous chunk has not yet - * ended, or the end of stream has been reached, does nothing. - * - * @return the length of the chunk, or -1 if the end of stream - * has been reached - * @throws IOException if an IO error occurs or the stream is corrupt - */ - protected long initChunk() throws IOException { - if (limit == 0) { // finished previous chunk - // read chunk-terminating CRLF if it's not the first chunk - if (initialized && readLine(in).length() > 0) - throw new IOException("chunk data must end with CRLF"); - initialized = true; - limit = parseChunkSize(readLine(in)); // read next chunk size - if (limit == 0) { // last chunk has size 0 - limit = -1; // mark end of stream - // read trailing headers, if any - Headers trailingHeaders = readHeaders(in); - if (headers != null) - headers.addAll(trailingHeaders); - } - } - return limit; - } - - /** - * Parses a chunk-size line. - * - * @param line the chunk-size line to parse - * @return the chunk size - * @throws IllegalArgumentException if the chunk-size line is invalid - */ - protected static long parseChunkSize(String line) throws IllegalArgumentException { - int pos = line.indexOf(';'); - line = pos < 0 ? line : line.substring(0, pos); // ignore params, if any - try { - return parseULong(line, 16); // throws NFE - } catch (NumberFormatException nfe) { - throw new IllegalArgumentException( - "invalid chunk size line: \"" + line + "\""); - } - } - } - - /** - * The {@code ChunkedOutputStream} encodes an OutputStream with the - * "chunked" transfer encoding. It should be used only when the content - * length is not known in advance, and with the response Transfer-Encoding - * header set to "chunked". - *

- * Data is written to the stream by calling the {@link #write(byte[], int, int)} - * method, which writes a new chunk per invocation. To end the stream, - * the {@link #writeTrailingChunk} method must be called or the stream closed. - */ - public static class ChunkedOutputStream extends FilterOutputStream { - - protected int state; // the current stream state - - /** - * Constructs a ChunkedOutputStream with the given underlying stream. - * - * @param out the underlying output stream to which the chunked stream - * is written - * @throws NullPointerException if the given stream is null - */ - public ChunkedOutputStream(OutputStream out) { - super(out); - if (out == null) - throw new NullPointerException("output stream is null"); - } - - /** - * Initializes a new chunk with the given size. - * - * @param size the chunk size (must be positive) - * @throws IllegalArgumentException if size is negative - * @throws IOException if an IO error occurs, or the stream has - * already been ended - */ - protected void initChunk(long size) throws IOException { - if (size < 0) - throw new IllegalArgumentException("invalid size: " + size); - if (state > 0) - out.write(CRLF); // end previous chunk - else if (state == 0) - state = 1; // start first chunk - else - throw new IOException("chunked stream has already ended"); - out.write(getBytes(Long.toHexString(size))); - out.write(CRLF); - } - - /** - * Writes the trailing chunk which marks the end of the stream. - * - * @param headers the (optional) trailing headers to write, or null - * @throws IOException if an error occurs - */ - public void writeTrailingChunk(Headers headers) throws IOException { - initChunk(0); // zero-sized chunk marks the end of the stream - if (headers == null) - out.write(CRLF); // empty header block - else - headers.writeTo(out); - state = -1; - } - - /** - * Writes a chunk containing the given byte. This method initializes - * a new chunk of size 1, and then writes the byte as the chunk data. - * - * @param b the byte to write as a chunk - * @throws IOException if an error occurs - */ - @Override - public void write(int b) throws IOException { - write(new byte[] { (byte)b }, 0, 1); - } - - /** - * Writes a chunk containing the given bytes. This method initializes - * a new chunk of the given size, and then writes the chunk data. - * - * @param b an array containing the bytes to write - * @param off the offset within the array where the data starts - * @param len the length of the data in bytes - * @throws IOException if an error occurs - * @throws IndexOutOfBoundsException if the given offset or length - * are outside the bounds of the given array - */ - @Override - public void write(byte[] b, int off, int len) throws IOException { - if (len > 0) // zero-sized chunk is the trailing chunk - initChunk(len); - out.write(b, off, len); - } - - /** - * Writes the trailing chunk if necessary, and closes the underlying stream. - * - * @throws IOException if an error occurs - */ - @Override - public void close() throws IOException { - if (state > -1) - writeTrailingChunk(null); - super.close(); - } - } - - /** - * The {@code MultipartInputStream} decodes an InputStream whose data has - * a "multipart/*" content type (see RFC 2046), providing the underlying - * data of its various parts. - *

- * The {@code InputStream} methods (e.g. {@link #read}) relate only to - * the current part, and the {@link #nextPart} method advances to the - * beginning of the next part. - */ - public static class MultipartInputStream extends FilterInputStream { - - protected final byte[] boundary; // including leading CRLF-- - protected final byte[] buf = new byte[4096]; - protected int head, tail; // indices of current part's data in buf - protected int end; // last index of input data read into buf - protected int len; // length of found boundary - protected int state; // initial, started data, start boundary, EOS, last boundary, epilogue - - /** - * Constructs a MultipartInputStream with the given underlying stream. - * - * @param in the underlying multipart stream - * @param boundary the multipart boundary - * @throws NullPointerException if the given stream or boundary is null - * @throws IllegalArgumentException if the given boundary's size is not - * between 1 and 70 - */ - protected MultipartInputStream(InputStream in, byte[] boundary) { - super(in); - int len = boundary.length; - if (len == 0 || len > 70) - throw new IllegalArgumentException("invalid boundary length"); - this.boundary = new byte[len + 4]; // CRLF--boundary - System.arraycopy(CRLF, 0, this.boundary, 0, 2); - this.boundary[2] = this.boundary[3] = '-'; - System.arraycopy(boundary, 0, this.boundary, 4, len); - } - - @Override - public int read() throws IOException { - if (!fill()) - return -1; - return buf[head++] & 0xFF; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (!fill()) - return -1; - len = Math.min(tail - head, len); - System.arraycopy(buf, head, b, off, len); // throws IOOBE as necessary - head += len; - return len; - } - - @Override - public long skip(long len) throws IOException { - if (len <= 0 || !fill()) - return 0; - len = Math.min(tail - head, len); - head += len; - return len; - } - - @Override - public int available() throws IOException { - return tail - head; - } - - @Override - public boolean markSupported() { - return false; - } - - /** - * Advances the stream position to the beginning of the next part. - * Data read before calling this method for the first time is the preamble, - * and data read after this method returns false is the epilogue. - * - * @return true if successful, or false if there are no more parts - * @throws IOException if an error occurs - */ - public boolean nextPart() throws IOException { - while (skip(buf.length) != 0); // skip current part (until boundary) - head = tail += len; // the next part starts right after boundary - state |= 1; // started data (after first boundary) - if (state >= 8) { // found last boundary - state |= 0x10; // now beyond last boundary (epilogue) - return false; - } - findBoundary(); // update indices - return true; - } - - /** - * Fills the buffer with more data from the underlying stream. - * - * @return true if there is available data for the current part, - * or false if the current part's end has been reached - * @throws IOException if an error occurs or the input format is invalid - */ - protected boolean fill() throws IOException { - // check if we already have more available data - if (head != tail) // remember that if we continue, head == tail below - return true; - // if there's no more room, shift extra unread data to beginning of buffer - if (tail > buf.length - 256) { // max boundary + whitespace supported size - System.arraycopy(buf, tail, buf, 0, end -= tail); - head = tail = 0; - } - // read more data and look for boundary (or potential partial boundary) - int read; - do { - read = super.read(buf, end, buf.length - end); - if (read < 0) - state |= 4; // end of stream (EOS) - else - end += read; - findBoundary(); // updates tail and length to next potential boundary - // if we found a partial boundary with no data before it, we must - // continue reading to determine if there is more data or not - } while (read > 0 && tail == head && len == 0); - // update and validate state - if (tail != 0) // anything but a boundary right at the beginning - state |= 1; // started data (preamble or after boundary) - if (state < 8 && len > 0) - state |= 2; // found start boundary - if ((state & 6) == 4 // EOS but no start boundary found - || len == 0 && ((state & 0xFC) == 4 // EOS but no last and no more boundaries - || read == 0 && tail == head)) // boundary longer than buffer - throw new IOException("missing boundary"); - if (state >= 0x10) // in epilogue - tail = end; // ignore boundaries, return everything - return tail > head; // available data in current part - } - - /** - * Finds the first (potential) boundary within the buffer's remaining data. - * Updates tail, length and state fields accordingly. - * - * @throws IOException if an error occurs or the input format is invalid - */ - protected void findBoundary() throws IOException { - // see RFC2046#5.1.1 for boundary syntax - len = 0; - int off = tail - ((state & 1) != 0 || buf[0] != '-' ? 0 : 2); // skip initial CRLF? - for (int end = this.end; tail < end; tail++, off = tail) { - int j = tail; // end of potential boundary - // try to match boundary value (leading CRLF is optional at first boundary) - while (j < end && j - off < boundary.length && buf[j] == boundary[j - off]) - j++; - // return potential partial boundary which is cut off at end of current data - if (j + 1 >= end) // at least two more chars needed for full boundary (CRLF or --) - return; - // if we found the boundary value, expand selection to include full line - if (j - off == boundary.length) { - // check if last boundary of entire multipart - if (buf[j] == '-' && buf[j + 1] == '-') { - j += 2; - state |= 8; // found last boundary that ends multipart - } - // allow linear whitespace after boundary - while (j < end && (buf[j] == ' ' || buf[j] == '\t')) - j++; - // check for CRLF (required, except in last boundary with no epilogue) - if (j + 1 < end && buf[j] == '\r' && buf[j + 1] == '\n') // found CRLF - len = j - tail + 2; // including optional whitespace and CRLF - else if (j + 1 < end || (state & 4) != 0 && j + 1 == end) // should have found or never will - throw new IOException("boundary must end with CRLF"); - else if ((state & 4) != 0) // last boundary with no CRLF at end of data is valid - len = j - tail; - return; - } - } - } - } - - /** - * The {@code MultipartIterator} iterates over the parts of a multipart/form-data request. - *

- * For example, to support file upload from a web browser: - *

    - *
  1. Create an HTML form which includes an input field of type "file", attributes - * method="post" and enctype="multipart/form-data", and an action URL of your choice, - * for example action="/upload". This form can be served normally like any other - * resource, e.g. from an HTML file on disk. - *
  2. Add a context handler for the action path ("/upload" in this example), using either - * the explicit {@link VirtualHost#addContext} method or the {@link Context} annotation. - *
  3. In the context handler implementation, construct a {@code MultipartIterator} from - * the client {@code Request}. - *
  4. Iterate over the form {@link Part}s, processing each named field as appropriate - - * for the file input field, read the uploaded file using the body input stream. - *
- */ - public static class MultipartIterator implements Iterator { - - /** - * The {@code Part} class encapsulates a single part of the multipart. - */ - public static class Part { - - public String name; - public String filename; - public Headers headers; - public InputStream body; - - /** - * Returns the part's name (form field name). - * - * @return the part's name - */ - public String getName() { return name; } - - /** - * Returns the part's filename (original filename entered in file form field). - * - * @return the part's filename, or null if there is none - */ - public String getFilename() { return filename; } - - /** - * Returns the part's headers. - * - * @return the part's headers - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the part's body (form field value). - * - * @return the part's body - */ - public InputStream getBody() { return body; } - - /*** - * Returns the part's body as a string. If the part - * headers do not specify a charset, UTF-8 is used. - * - * @return the part's body as a string - * @throws IOException if an IO error occurs - */ - public String getString() throws IOException { - String charset = headers.getParams("Content-Type").get("charset"); - return readToken(body, -1, charset == null ? "UTF-8" : charset, 8192); - } - } - - protected final MultipartInputStream in; - protected boolean next; - - /** - * Creates a new MultipartIterator from the given request. - * - * @param req the multipart/form-data request - * @throws IOException if an IO error occurs - * @throws IllegalArgumentException if the given request's content type - * is not multipart/form-data, or is missing the boundary - */ - public MultipartIterator(Request req) throws IOException { - Map ct = req.getHeaders().getParams("Content-Type"); - if (!ct.containsKey("multipart/form-data")) - throw new IllegalArgumentException("Content-Type is not multipart/form-data"); - String boundary = ct.get("boundary"); // should be US-ASCII - if (boundary == null) - throw new IllegalArgumentException("Content-Type is missing boundary"); - in = new MultipartInputStream(req.getBody(), getBytes(boundary)); - } - - public boolean hasNext() { - try { - return next || (next = in.nextPart()); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - - public Part next() { - if (!hasNext()) - throw new NoSuchElementException(); - next = false; - Part p = new Part(); - try { - p.headers = readHeaders(in); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - Map cd = p.headers.getParams("Content-Disposition"); - p.name = cd.get("name"); - p.filename = cd.get("filename"); - p.body = in; - return p; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } - - /** - * The {@code VirtualHost} class represents a virtual host in the server. - */ - public static class VirtualHost { - - public static final String DEFAULT_HOST_NAME = "~DEFAULT~"; - - /** - * The {@code ContextInfo} class holds a single context's information. - */ - public class ContextInfo { - - protected final String path; - protected final Map handlers = - new ConcurrentHashMap(2); - - /** - * Constructs a ContextInfo with the given context path. - * - * @param path the context path (without trailing slash) - */ - public ContextInfo(String path) { - this.path = path; - } - - /** - * Returns the context path. - * - * @return the context path, or null if there is none - */ - public String getPath() { - return path; - } - - /** - * Returns the map of supported HTTP methods and their corresponding handlers. - * - * @return the map of supported HTTP methods and their corresponding handlers - */ - public Map getHandlers() { - return handlers; - } - - /** - * Adds (or replaces) a context handler for the given HTTP methods. - * - * @param handler the context handler - * @param methods the HTTP methods supported by the handler (default is "GET") - */ - public void addHandler(ContextHandler handler, String... methods) { - if (methods.length == 0) - methods = new String[] { "GET" }; - for (String method : methods) { - handlers.put(method, handler); - VirtualHost.this.methods.add(method); // it's now supported by server - } - } - } - - protected final String name; - protected final Set aliases = new CopyOnWriteArraySet(); - protected volatile String directoryIndex = "index.html"; - protected volatile boolean allowGeneratedIndex; - protected final Set methods = new CopyOnWriteArraySet(); - protected final ContextInfo emptyContext = new ContextInfo(null); - protected final ConcurrentMap contexts = - new ConcurrentHashMap(); - - /** - * Constructs a VirtualHost with the given name. - * - * @param name the host's name, or null if it is the default host - */ - public VirtualHost(String name) { - this.name = name; - contexts.put("*", new ContextInfo(null)); // for "OPTIONS *" - } - - /** - * Returns this host's name. - * - * @return this host's name, or null if it is the default host - */ - public String getName() { - return name; - } - - /** - * Adds an alias for this host. - * - * @param alias the alias - */ - public void addAlias(String alias) { - aliases.add(alias); - } - - /** - * Returns this host's aliases. - * - * @return the (unmodifiable) set of aliases (which may be empty) - */ - public Set getAliases() { - return Collections.unmodifiableSet(aliases); - } - - /** - * Sets the directory index file. For every request whose URI ends with - * a '/' (i.e. a directory), the index file is appended to the path, - * and the resulting resource is served if it exists. If it does not - * exist, an auto-generated index for the requested directory may be - * served, depending on whether {@link #setAllowGeneratedIndex - * a generated index is allowed}, otherwise an error is returned. - * The default directory index file is "index.html". - * - * @param directoryIndex the directory index file, or null if no - * index file should be used - */ - public void setDirectoryIndex(String directoryIndex) { - this.directoryIndex = directoryIndex; - } - - /** - * Gets this host's directory index file. - * - * @return the directory index file, or null - */ - public String getDirectoryIndex() { - return directoryIndex; - } - - /** - * Sets whether auto-generated indices are allowed. If false, and a - * directory resource is requested, an error will be returned instead. - * - * @param allowed specifies whether generated indices are allowed - */ - public void setAllowGeneratedIndex(boolean allowed) { - this.allowGeneratedIndex = allowed; - } - - /** - * Returns whether auto-generated indices are allowed. - * - * @return whether auto-generated indices are allowed - */ - public boolean isAllowGeneratedIndex() { - return allowGeneratedIndex; - } - - /** - * Returns all HTTP methods explicitly supported by at least one context - * (this may or may not include the methods with required or built-in support). - * - * @return all HTTP methods explicitly supported by at least one context - */ - public Set getMethods() { - return methods; - } - - /** - * Returns the context handler for the given path. - * - * If a context is not found for the given path, the search is repeated for - * its parent path, and so on until a base context is found. If neither the - * given path nor any of its parents has a context, an empty context is returned. - * - * @param path the context's path - * @return the context info for the given path, or an empty context if none exists - */ - public ContextInfo getContext(String path) { - // all context paths are without trailing slash - for (path = trimRight(path, '/'); path != null; path = getParentPath(path)) { - ContextInfo info = contexts.get(path); - if (info != null) - return info; - } - return emptyContext; - } - - /** - * Adds a context and its corresponding context handler to this server. - * Paths are normalized by removing trailing slashes (except the root). - * - * @param path the context's path (must start with '/') - * @param handler the context handler for the given path - * @param methods the HTTP methods supported by the context handler (default is "GET") - * @throws IllegalArgumentException if path is malformed - */ - public void addContext(String path, ContextHandler handler, String... methods) { - if (path == null || !path.startsWith("/") && !path.equals("*")) - throw new IllegalArgumentException("invalid path: " + path); - path = trimRight(path, '/'); // remove trailing slash - ContextInfo info = new ContextInfo(path); - ContextInfo existing = contexts.putIfAbsent(path, info); - info = existing != null ? existing : info; - info.addHandler(handler, methods); - } - - /** - * Adds contexts for all methods of the given object that - * are annotated with the {@link Context} annotation. - * - * @param o the object whose annotated methods are added - * @throws IllegalArgumentException if a Context-annotated - * method has an {@link Context invalid signature} - */ - public void addContexts(Object o) throws IllegalArgumentException { - for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { - // add to contexts those with @Context annotation - for (Method m : c.getDeclaredMethods()) { - Context context = m.getAnnotation(Context.class); - if (context != null) { - m.setAccessible(true); // allow access to private method - ContextHandler handler = new MethodContextHandler(m, o); - addContext(context.value(), handler, context.methods()); - } - } - } - } - } - - /** - * The {@code Context} annotation decorates methods which are mapped - * to a context (path) within the server, and provide its contents. - *

- * The annotated methods must have the same signature and contract - * as {@link ContextHandler#serve}, but can have arbitrary names. - * - * @see VirtualHost#addContexts(Object) - */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - public @interface Context { - - /** - * The context (path) that this field maps to (must begin with '/'). - * - * @return the context (path) that this field maps to - */ - String value(); - - /** - * The HTTP methods supported by this context handler (default is "GET"). - * - * @return the HTTP methods supported by this context handler - */ - String[] methods() default "GET"; - } - - /** - * A {@code ContextHandler} serves the content of resources within a context. - * - * @see VirtualHost#addContext - */ - public interface ContextHandler { - - /** - * Serves the given request using the given response. - * - * @param req the request to be served - * @param resp the response to be filled - * @return an HTTP status code, which will be used in returning - * a default response appropriate for this status. If this - * method invocation already sent anything in the response - * (headers or content), it must return 0, and no further - * processing will be done - * @throws IOException if an IO error occurs - */ - int serve(Request req, Response resp) throws IOException; - } - - /** - * The {@code FileContextHandler} services a context by mapping it - * to a file or folder (recursively) on disk. - */ - public static class FileContextHandler implements ContextHandler { - - protected final File base; - - public FileContextHandler(File dir) throws IOException { - this.base = dir.getCanonicalFile(); - } - - public int serve(Request req, Response resp) throws IOException { - return serveFile(base, req.getContext().getPath(), req, resp); - } - } - - /** - * The {@code MethodContextHandler} services a context - * by invoking a handler method on a specified object. - *

- * The method must have the same signature and contract as - * {@link ContextHandler#serve}, but can have an arbitrary name. - * - * @see VirtualHost#addContexts(Object) - */ - public static class MethodContextHandler implements ContextHandler { - - protected final Method m; - protected final Object obj; - - public MethodContextHandler(Method m, Object obj) throws IllegalArgumentException { - this.m = m; - this.obj = obj; - Class[] params = m.getParameterTypes(); - if (params.length != 2 - || !Request.class.isAssignableFrom(params[0]) - || !Response.class.isAssignableFrom(params[1]) - || !int.class.isAssignableFrom(m.getReturnType())) - throw new IllegalArgumentException("invalid method signature: " + m); - } - - public int serve(Request req, Response resp) throws IOException { - try { - return (Integer)m.invoke(obj, req, resp); - } catch (InvocationTargetException ite) { - throw new IOException("error: " + ite.getCause().getMessage()); - } catch (Exception e) { - throw new IOException("error: " + e); - } - } - } - - /** - * The {@code Header} class encapsulates a single HTTP header. - */ - public static class Header { - - protected final String name; - protected final String value; - - /** - * Constructs a header with the given name and value. - * Leading and trailing whitespace are trimmed. - * - * @param name the header name - * @param value the header value - * @throws NullPointerException if name or value is null - * @throws IllegalArgumentException if name is empty - */ - public Header(String name, String value) { - this.name = name.trim(); - this.value = value.trim(); - // RFC2616#14.23 - header can have an empty value (e.g. Host) - if (this.name.length() == 0) // but name cannot be empty - throw new IllegalArgumentException("name cannot be empty"); - } - - /** - * Returns this header's name. - * - * @return this header's name - */ - public String getName() { return name; } - - /** - * Returns this header's value. - * - * @return this header's value - */ - public String getValue() { return value; } - } - - /** - * The {@code Headers} class encapsulates a collection of HTTP headers. - * - * Header names are treated case-insensitively, although this class retains - * their original case. Header insertion order is maintained as well. - */ - public static class Headers implements Iterable

{ - - // due to the requirements of case-insensitive name comparisons, - // retaining the original case, and retaining header insertion order, - // and due to the fact that the number of headers is generally - // quite small (usually under 12 headers), we use a simple array with - // linear access times, which proves to be more efficient and - // straightforward than the alternatives - protected Header[] headers = new Header[12]; - protected int count; - - /** - * Returns the number of added headers. - * - * @return the number of added headers - */ - public int size() { - return count; - } - - /** - * Returns the value of the first header with the given name. - * - * @param name the header name (case insensitive) - * @return the header value, or null if none exists - */ - public String get(String name) { - for (int i = 0; i < count; i++) - if (headers[i].getName().equalsIgnoreCase(name)) - return headers[i].getValue(); - return null; - } - - /** - * Returns the Date value of the header with the given name. - * - * @param name the header name (case insensitive) - * @return the header value as a Date, or null if none exists - * or if the value is not in any supported date format - */ - public Date getDate(String name) { - try { - String header = get(name); - return header == null ? null : parseDate(header); - } catch (IllegalArgumentException iae) { - return null; - } - } - - /** - * Returns whether there exists a header with the given name. - * - * @param name the header name (case insensitive) - * @return whether there exists a header with the given name - */ - public boolean contains(String name) { - return get(name) != null; - } - - /** - * Adds a header with the given name and value to the end of this - * collection of headers. Leading and trailing whitespace are trimmed. - * - * @param name the header name (case insensitive) - * @param value the header value - */ - public void add(String name, String value) { - Header header = new Header(name, value); // also validates - // expand array if necessary - if (count == headers.length) { - Header[] expanded = new Header[2 * count]; - System.arraycopy(headers, 0, expanded, 0, count); - headers = expanded; - } - headers[count++] = header; // inlining header would cause a bug! - } - - /** - * Adds all given headers to the end of this collection of headers, - * in their original order. - * - * @param headers the headers to add - */ - public void addAll(Headers headers) { - for (Header header : headers) - add(header.getName(), header.getValue()); - } - - /** - * Adds a header with the given name and value, replacing the first - * existing header with the same name. If there is no existing header - * with the same name, it is added as in {@link #add}. - * - * @param name the header name (case insensitive) - * @param value the header value - * @return the replaced header, or null if none existed - */ - public Header replace(String name, String value) { - for (int i = 0; i < count; i++) { - if (headers[i].getName().equalsIgnoreCase(name)) { - Header prev = headers[i]; - headers[i] = new Header(name, value); - return prev; - } - } - add(name, value); - return null; - } - - /** - * Removes all headers with the given name (if any exist). - * - * @param name the header name (case insensitive) - */ - public void remove(String name) { - int j = 0; - for (int i = 0; i < count; i++) - if (!headers[i].getName().equalsIgnoreCase(name)) - headers[j++] = headers[i]; - while (count > j) - headers[--count] = null; - } - - /** - * Writes the headers to the given stream (including trailing CRLF). - * - * @param out the stream to write the headers to - * @throws IOException if an error occurs - */ - public void writeTo(OutputStream out) throws IOException { - for (int i = 0; i < count; i++) { - out.write(getBytes(headers[i].getName(), ": ", headers[i].getValue())); - out.write(CRLF); - } - out.write(CRLF); // ends header block - } - - /** - * Returns a header's parameters. Parameter order is maintained, - * and the first key (in iteration order) is the header's value - * without the parameters. - * - * @param name the header name (case insensitive) - * @return the header's parameter names and values - */ - public Map getParams(String name) { - Map params = new LinkedHashMap(); - for (String param : split(get(name), ";", -1)) { - String[] pair = split(param, "=", 2); - String val = pair.length == 1 ? "" : trimLeft(trimRight(pair[1], '"'), '"'); - params.put(pair[0], val); - } - return params; - } - - /** - * Returns an iterator over the headers, in their insertion order. - * If the headers collection is modified during iteration, the - * iteration result is undefined. The remove operation is unsupported. - * - * @return an Iterator over the headers - */ - public Iterator
iterator() { - // we use the built-in wrapper instead of a trivial custom implementation - // since even a tiny anonymous class here compiles to a 1.5K class file - return Arrays.asList(headers).subList(0, count).iterator(); - } - } - - /** - * The {@code Request} class encapsulates a single HTTP request. - */ - public class Request { - - protected String method; - protected URI uri; - protected URL baseURL; // cached value - protected String version; - protected Headers headers; - protected InputStream body; - protected Map params; // cached value - protected VirtualHost host; // cached value - protected VirtualHost.ContextInfo context; // cached value - - /** - * Constructs a Request from the data in the given input stream. - * - * @param in the input stream from which the request is read - * @throws IOException if an error occurs - */ - public Request(InputStream in) throws IOException { - readRequestLine(in); - headers = readHeaders(in); - // RFC2616#3.6 - if "chunked" is used, it must be the last one - // RFC2616#4.4 - if non-identity Transfer-Encoding is present, - // it must either include "chunked" or close the connection after - // the body, and in any case ignore Content-Length. - // if there is no such Transfer-Encoding, use Content-Length - // if neither header exists, there is no body - String header = headers.get("Transfer-Encoding"); - if (header != null && !header.toLowerCase(Locale.US).equals("identity")) { - if (Arrays.asList(splitElements(header, true)).contains("chunked")) - body = new ChunkedInputStream(in, headers); - else - body = in; // body ends when connection closes - } else { - header = headers.get("Content-Length"); - long len = header == null ? 0 : parseULong(header, 10); - body = new LimitedInputStream(in, len, false); - } - } - - /** - * Returns the request method. - * - * @return the request method - */ - public String getMethod() { return method; } - - /** - * Returns the request URI. - * - * @return the request URI - */ - public URI getURI() { return uri; } - - /** - * Returns the request version string. - * - * @return the request version string - */ - public String getVersion() { return version; } - - /** - * Returns the request headers. - * - * @return the request headers - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the input stream containing the request body. - * - * @return the input stream containing the request body - */ - public InputStream getBody() { return body; } - - /** - * Returns the body in a String format - */ - public String getJson(){ - java.util.Scanner s = new java.util.Scanner(body).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } - - /** - * Returns the path component of the request URI, after - * URL decoding has been applied (using the UTF-8 charset). - * - * @return the decoded path component of the request URI - */ - public String getPath() { - return uri.getPath(); - } - - /** - * Sets the path component of the request URI. This can be useful - * in URL rewriting, etc. - * - * @param path the path to set - * @throws IllegalArgumentException if the given path is malformed - */ - public void setPath(String path) { - try { - uri = new URI(uri.getScheme(), uri.getHost(), - trimDuplicates(path, '/'), uri.getFragment()); - context = null; // clear cached context so it will be recalculated - } catch (URISyntaxException use) { - throw new IllegalArgumentException("error setting path", use); - } - } - - /** - * Returns the base URL (scheme, host and port) of the request resource. - * The host name is taken from the request URI or the Host header or a - * default host (see RFC2616#5.2). - * - * @return the base URL of the requested resource, or null if it - * is malformed - */ - public URL getBaseURL() { - if (baseURL != null) - return baseURL; - // normalize host header - String host = uri.getHost(); - if (host == null) { - host = headers.get("Host"); - if (host == null) // missing in HTTP/1.0 - host = detectLocalHostName(); - } - int pos = host.indexOf(':'); - host = pos < 0 ? host : host.substring(0, pos); - try { - return baseURL = new URL(secure ? "https" : "http", host, port, ""); - } catch (MalformedURLException mue) { - return null; - } - } - - /** - * Returns the request parameters, which are parsed both from the query - * part of the request URI, and from the request body if its content - * type is "application/x-www-form-urlencoded" (i.e. a submitted form). - * UTF-8 encoding is assumed in both cases. - *

- * The parameters are returned as a list of string arrays, each containing - * the parameter name as the first element and its corresponding value - * as the second element (or an empty string if there is no value). - *

- * The list retains the original order of the parameters. - * - * @return the request parameters name-value pairs, - * or an empty list if there are none - * @throws IOException if an error occurs - * @see HTTPServer#parseParamsList(String) - */ - public List getParamsList() throws IOException { - List queryParams = parseParamsList(uri.getRawQuery()); - List bodyParams = Collections.emptyList(); - String ct = headers.get("Content-Type"); - if (ct != null && ct.toLowerCase(Locale.US).startsWith("application/x-www-form-urlencoded")) - bodyParams = parseParamsList(readToken(body, -1, "UTF-8", 2097152)); // 2MB limit - if (bodyParams.isEmpty()) - return queryParams; - if (queryParams.isEmpty()) - return bodyParams; - queryParams.addAll(bodyParams); - return queryParams; - } - - /** - * Returns the request parameters, which are parsed both from the query - * part of the request URI, and from the request body if its content - * type is "application/x-www-form-urlencoded" (i.e. a submitted form). - * UTF-8 encoding is assumed in both cases. - *

- * For multivalued parameters (i.e. multiple parameters with the same - * name), only the first one is considered. For access to all values, - * use {@link #getParamsList()} instead. - *

- * The map iteration retains the original order of the parameters. - * - * @return the request parameters name-value pairs, - * or an empty map if there are none - * @throws IOException if an error occurs - * @see #getParamsList() - */ - public Map getParams() throws IOException { - if (params == null) - params = toMap(getParamsList()); - return params; - } - - /** - * Returns the absolute (zero-based) content range value read - * from the Range header. If multiple ranges are requested, a single - * range containing all of them is returned. - * - * @param length the full length of the requested resource - * @return the requested range, or null if the Range header - * is missing or invalid - */ - public long[] getRange(long length) { - String header = headers.get("Range"); - return header == null || !header.startsWith("bytes=") - ? null : parseRange(header.substring(6), length); - } - - /** - * Reads the request line, parsing the method, URI and version string. - * - * @param in the input stream from which the request line is read - * @throws IOException if an error occurs or the request line is invalid - */ - protected void readRequestLine(InputStream in) throws IOException { - // RFC2616#4.1: should accept empty lines before request line - // RFC2616#19.3: tolerate additional whitespace between tokens - String line; - try { - do { line = readLine(in); } while (line.length() == 0); - } catch (IOException ioe) { // if EOF, timeout etc. - throw new IOException("missing request line"); // signal that the request did not begin - } - String[] tokens = split(line, " ", -1); - if (tokens.length != 3) - throw new IOException("invalid request line: \"" + line + "\""); - try { - method = tokens[0]; - // must remove '//' prefix which constructor parses as host name - uri = new URI(trimDuplicates(tokens[1], '/')); - version = tokens[2]; // RFC2616#2.1: allow implied LWS; RFC7230#3.1.1: disallow it - } catch (URISyntaxException use) { - throw new IOException("invalid URI: " + use.getMessage()); - } - } - - /** - * Returns the virtual host corresponding to the requested host name, - * or the default host if none exists. - * - * @return the virtual host corresponding to the requested host name, - * or the default virtual host - */ - public VirtualHost getVirtualHost() { - return host != null ? host - : (host = HTTPServer.this.getVirtualHost(getBaseURL().getHost())) != null ? host - : (host = HTTPServer.this.getVirtualHost(null)); - } - - /** - * Returns the info of the context handling this request. - * - * @return the info of the context handling this request, or an empty context - */ - public VirtualHost.ContextInfo getContext() { - return context != null ? context : (context = getVirtualHost().getContext(getPath())); - } - } - - /** - * The {@code Response} class encapsulates a single HTTP response. - */ - public class Response implements Closeable { - - protected OutputStream out; // the underlying output stream - protected OutputStream[] encoders = new OutputStream[4]; // chained encoder streams - protected Headers headers; - protected boolean discardBody; - protected int state; // nothing sent, headers sent, or closed - protected Request req; // request used in determining client capabilities - - /** - * Constructs a Response whose output is written to the given stream. - * - * @param out the stream to which the response is written - */ - public Response(OutputStream out) { - this.out = out; - this.headers = new Headers(); - } - - /** - * Sets whether this response's body is discarded or sent. - * - * @param discardBody specifies whether the body is discarded or not - */ - public void setDiscardBody(boolean discardBody) { - this.discardBody = discardBody; - } - - /** - * Sets the request which is used in determining the capabilities - * supported by the client (e.g. compression, encoding, etc.) - * - * @param req the request - */ - public void setClientCapabilities(Request req) { this.req = req; } - - /** - * Returns the request headers collection. - * - * @return the request headers collection - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the underlying output stream to which the response is written. - * Except for special cases, you should use {@link #getBody()} instead. - * - * @return the underlying output stream to which the response is written - */ - public OutputStream getOutputStream() { return out; } - - /** - * Returns whether the response headers were already sent. - * - * @return whether the response headers were already sent - */ - public boolean headersSent() { return state == 1; } - - /** - * Returns an output stream into which the response body can be written. - * The stream applies encodings (e.g. compression) according to the sent headers. - * This method must be called after response headers have been sent - * that indicate there is a body. Normally, the content should be - * prepared (not sent) even before the headers are sent, so that any - * errors during processing can be caught and a proper error response returned - - * after the headers are sent, it's too late to change the status into an error. - * - * @return an output stream into which the response body can be written, - * or null if the body should not be written (e.g. it is discarded) - * @throws IOException if an error occurs - */ - public OutputStream getBody() throws IOException { - if (encoders[0] != null || discardBody) - return encoders[0]; // return the existing stream (or null) - // set up chain of encoding streams according to headers - List te = Arrays.asList(splitElements(headers.get("Transfer-Encoding"), true)); - List ce = Arrays.asList(splitElements(headers.get("Content-Encoding"), true)); - int i = encoders.length - 1; - encoders[i] = new FilterOutputStream(out) { - @Override - public void close() {} // keep underlying connection stream open for now - @Override // override the very inefficient default implementation - public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } - }; - if (te.contains("chunked")) - encoders[--i] = new ChunkedOutputStream(encoders[i + 1]); - if (ce.contains("gzip") || te.contains("gzip")) - encoders[--i] = new GZIPOutputStream(encoders[i + 1], 4096); - else if (ce.contains("deflate") || te.contains("deflate")) - encoders[--i] = new DeflaterOutputStream(encoders[i + 1]); - encoders[0] = encoders[i]; - encoders[i] = null; // prevent duplicate reference - return encoders[0]; // returned stream is always first - } - - /** - * Closes this response and flushes all output. - * - * @throws IOException if an error occurs - */ - public void close() throws IOException { - state = -1; // closed - if (encoders[0] != null) - encoders[0].close(); // close all chained streams (except the underlying one) - out.flush(); // always flush underlying stream (even if getBody was never called) - } - - /** - * Sends the response headers with the given response status. - * A Date header is added if it does not already exist. - * If the response has a body, the Content-Length/Transfer-Encoding - * and Content-Type headers must be set before sending the headers. - * - * @param status the response status - * @throws IOException if an error occurs or headers were already sent - * @see #sendHeaders(int, long, long, String, String, long[]) - */ - public void sendHeaders(int status) throws IOException { - if (headersSent()) - throw new IOException("headers were already sent"); - if (!headers.contains("Date")) - headers.add("Date", formatDate(System.currentTimeMillis())); - headers.add("Server", "JLHTTP/2.4"); - out.write(getBytes("HTTP/1.1 ", Integer.toString(status), " ", statuses[status])); - out.write(CRLF); - headers.writeTo(out); - state = 1; // headers sent - } - - /** - * Sends the response headers, including the given response status - * and description, and all response headers. If they do not already - * exist, the following headers are added as necessary: - * Content-Range, Content-Type, Transfer-Encoding, Content-Encoding, - * Content-Length, Last-Modified, ETag, Connection and Date. Ranges are - * properly calculated as well, with a 200 status changed to a 206 status. - * - * @param status the response status - * @param length the response body length, or zero if there is no body, - * or negative if there is a body but its length is not yet known - * @param lastModified the last modified date of the response resource, - * or non-positive if unknown. A time in the future will be - * replaced with the current system time. - * @param etag the ETag of the response resource, or null if unknown - * (see RFC2616#3.11) - * @param contentType the content type of the response resource, or null - * if unknown (in which case "application/octet-stream" will be sent) - * @param range the content range that will be sent, or null if the - * entire resource will be sent - * @throws IOException if an error occurs - */ - public void sendHeaders(int status, long length, long lastModified, - String etag, String contentType, long[] range) throws IOException { - if (range != null) { - headers.add("Content-Range", "bytes " + range[0] + "-" + - range[1] + "/" + (length >= 0 ? length : "*")); - length = range[1] - range[0] + 1; - if (status == 200) - status = 206; - } - String ct = headers.get("Content-Type"); - if (ct == null) { - ct = contentType != null ? contentType : "application/octet-stream"; - headers.add("Content-Type", ct); - } - if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding")) { - // RFC2616#3.6: transfer encodings are case-insensitive and must not be sent to an HTTP/1.0 client - boolean modern = req != null && req.getVersion().endsWith("1.1"); - String accepted = req == null ? null : req.getHeaders().get("Accept-Encoding"); - List encodings = Arrays.asList(splitElements(accepted, true)); - String compression = encodings.contains("gzip") ? "gzip" : - encodings.contains("deflate") ? "deflate" : null; - if (compression != null && (length < 0 || length > 300) && isCompressible(ct) && modern) { - headers.add("Transfer-Encoding", "chunked"); // compressed data is always unknown length - headers.add("Content-Encoding", compression); - } else if (length < 0 && modern) { - headers.add("Transfer-Encoding", "chunked"); // unknown length - } else if (length >= 0) { - headers.add("Content-Length", Long.toString(length)); // known length - } - } - if (!headers.contains("Vary")) // RFC7231#7.1.4: Vary field should include headers - headers.add("Vary", "Accept-Encoding"); // that are used in selecting representation - if (lastModified > 0 && !headers.contains("Last-Modified")) // RFC2616#14.29 - headers.add("Last-Modified", formatDate(Math.min(lastModified, System.currentTimeMillis()))); - if (etag != null && !headers.contains("ETag")) - headers.add("ETag", etag); - if (req != null && "close".equalsIgnoreCase(req.getHeaders().get("Connection")) - && !headers.contains("Connection")) - headers.add("Connection", "close"); // #RFC7230#6.6: should reply to close with close - sendHeaders(status); - } - - /** - * Sends the full response with the given status, and the given string - * as the body. The text is sent in the UTF-8 charset. If a - * Content-Type header was not explicitly set, it will be set to - * text/html, and so the text must contain valid (and properly - * {@link HTTPServer#escapeHTML escaped}) HTML. - * - * @param status the response status - * @param text the text body (sent as text/html) - * @throws IOException if an error occurs - */ - public void send(int status, String text) throws IOException { - byte[] content = text.getBytes("UTF-8"); - sendHeaders(status, content.length, -1, - "W/\"" + Integer.toHexString(text.hashCode()) + "\"", - "text/html; charset=utf-8", null); - OutputStream out = getBody(); - if (out != null) - out.write(content); - } - - public void send(int status, byte[] text) throws IOException { - //byte[] content = text.getBytes("UTF-8"); - byte[] content = text; - sendHeaders(status, content.length, -1, - "W/\"" + Integer.toHexString(text.hashCode()) + "\"", - "text/html; charset=utf-8", null); - OutputStream out = getBody(); - if (out != null) - out.write(content); - } - - /** - * Sends an error response with the given status and detailed message. - * An HTML body is created containing the status and its description, - * as well as the message, which is escaped using the - * {@link HTTPServer#escapeHTML escape} method. - * - * @param status the response status - * @param text the text body (sent as text/html) - * @throws IOException if an error occurs - */ - public void sendError(int status, String text) throws IOException { - send(status, String.format( - "%n%n%d %s%n" + - "

%d %s

%n

%s

%n", - status, statuses[status], status, statuses[status], escapeHTML(text))); - } - - /** - * Sends an error response with the given status and default body. - * - * @param status the response status - * @throws IOException if an error occurs - */ - public void sendError(int status) throws IOException { - String text = status < 400 ? ":)" : "sorry it didn't work out :("; - sendError(status, text); - } - - /** - * Sends the response body. This method must be called only after the - * response headers have been sent (and indicate that there is a body). - * - * @param body a stream containing the response body - * @param length the full length of the response body - * @param range the sub-range within the response body that should be - * sent, or null if the entire body should be sent - * @throws IOException if an error occurs - */ - public void sendBody(InputStream body, long length, long[] range) throws IOException { - OutputStream out = getBody(); - if (out != null) { - if (range != null) { - long offset = range[0]; - length = range[1] - range[0] + 1; - while (offset > 0) { - long skip = body.skip(offset); - if (skip == 0) - throw new IOException("can't skip to " + range[0]); - offset -= skip; - } - } - transfer(body, out, length); - } - } - - /** - * Sends a 301 or 302 response, redirecting the client to the given URL. - * - * @param url the absolute URL to which the client is redirected - * @param permanent specifies whether a permanent (301) or - * temporary (302) redirect status is sent - * @throws IOException if an IO error occurs or url is malformed - */ - public void redirect(String url, boolean permanent) throws IOException { - try { - url = new URI(url).toASCIIString(); - } catch (URISyntaxException e) { - throw new IOException("malformed URL: " + url); - } - headers.add("Location", url); - // some user-agents expect a body, so we send it - if (permanent) - sendError(301, "Permanently moved to " + url); - else - sendError(302, "Temporarily moved to " + url); - } - } - - /** - * The {@code SocketHandlerThread} handles accepted sockets. - */ - protected class SocketHandlerThread extends Thread { - @Override - public void run() { - setName(getClass().getSimpleName() + "-" + port); - try { - ServerSocket serv = HTTPServer.this.serv; // keep local to avoid NPE when stopped - while (serv != null && !serv.isClosed()) { - final Socket sock = serv.accept(); - executor.execute(new Runnable() { - public void run() { - try { - try { - sock.setSoTimeout(socketTimeout); - sock.setTcpNoDelay(true); // we buffer anyway, so improve latency - handleConnection(sock.getInputStream(), sock.getOutputStream()); - } finally { - try { - // RFC7230#6.6 - close socket gracefully - // (except SSL socket which doesn't support half-closing) - if (!(sock instanceof SSLSocket)) { - sock.shutdownOutput(); // half-close socket (only output) - transfer(sock.getInputStream(), null, -1); // consume input - } - } finally { - sock.close(); // and finally close socket fully - } - } - } catch (IOException ignore) {} - } - }); - } - } catch (IOException ignore) {} - } - } - - protected volatile int port; - protected volatile int socketTimeout = 10000; - protected volatile ServerSocketFactory serverSocketFactory; - protected volatile boolean secure; - protected volatile Executor executor; - protected volatile ServerSocket serv; - protected final Map hosts = new ConcurrentHashMap(); - - /** - * Constructs an HTTPServer which can accept connections on the given port. - * Note: the {@link #start()} method must be called to start accepting - * connections. - * - * @param port the port on which this server will accept connections - */ - public HTTPServer(int port) { - setPort(port); - addVirtualHost(new VirtualHost(null)); // add default virtual host - } - - /** - * Constructs an HTTPServer which can accept connections on the default HTTP port 80. - * Note: the {@link #start()} method must be called to start accepting connections. - */ - public HTTPServer() { - this(80); - } - - /** - * Sets the port on which this server will accept connections. - * - * @param port the port on which this server will accept connections - */ - public void setPort(int port) { - this.port = port; - } - - /** - * Sets the factory used to create the server socket. - * If null or not set, the default {@link ServerSocketFactory#getDefault()} is used. - * For secure sockets (HTTPS), use an SSLServerSocketFactory instance. - * The port should usually also be changed for HTTPS, e.g. port 443 instead of 80. - *

- * If using the default SSLServerSocketFactory returned by - * {@link SSLServerSocketFactory#getDefault()}, the appropriate system properties - * must be set to configure the default JSSE provider, such as - * {@code javax.net.ssl.keyStore} and {@code javax.net.ssl.keyStorePassword}. - * - * @param factory the server socket factory to use - */ - public void setServerSocketFactory(ServerSocketFactory factory) { - this.serverSocketFactory = factory; - this.secure = factory instanceof SSLServerSocketFactory; - } - - /** - * Sets the socket timeout for established connections. - * - * @param timeout the socket timeout in milliseconds - */ - public void setSocketTimeout(int timeout) { this.socketTimeout = timeout; } - - /** - * Sets the executor used in servicing HTTP connections. - * If null, a default executor is used. The caller is responsible - * for shutting down the provided executor when necessary. - * - * @param executor the executor to use - */ - public void setExecutor(Executor executor) { - this.executor = executor; - } - - /** - * Returns the virtual host with the given name. - * - * @param name the name of the virtual host to return, or null for - * the default virtual host - * @return the virtual host with the given name, or null if it doesn't exist - */ - public VirtualHost getVirtualHost(String name) { - return hosts.get(name == null ? VirtualHost.DEFAULT_HOST_NAME : name); - } - - /** - * Returns all virtual hosts. - * - * @return all virtual hosts (as an unmodifiable set) - */ - public Set getVirtualHosts() { - return Collections.unmodifiableSet(new HashSet(hosts.values())); - } - - /** - * Adds the given virtual host to the server. - * If the host's name or aliases already exist, they are overwritten. - * - * @param host the virtual host to add - */ - public void addVirtualHost(VirtualHost host) { - String name = host.getName(); - hosts.put(name == null ? VirtualHost.DEFAULT_HOST_NAME : name, host); - } - - /** - * Creates the server socket used to accept connections, using the configured - * {@link #setServerSocketFactory ServerSocketFactory} and {@link #setPort port}. - *

- * Cryptic errors seen here often mean the factory configuration details are wrong. - * - * @return the created server socket - * @throws IOException if the socket cannot be created - */ - protected ServerSocket createServerSocket() throws IOException { - ServerSocket serv = serverSocketFactory.createServerSocket(); - serv.setReuseAddress(true); - serv.bind(new InetSocketAddress(port)); - return serv; - } - - /** - * Starts this server. If it is already started, does nothing. - * Note: Once the server is started, configuration-altering methods - * of the server and its virtual hosts must not be used. To modify the - * configuration, the server must first be stopped. - * - * @throws IOException if the server cannot begin accepting connections - */ - public synchronized void start() throws IOException { - if (serv != null) - return; - if (serverSocketFactory == null) // assign default server socket factory if needed - serverSocketFactory = ServerSocketFactory.getDefault(); // plain sockets - serv = createServerSocket(); - if (executor == null) // assign default executor if needed - executor = Executors.newCachedThreadPool(); // consumes no resources when idle - // register all host aliases (which may have been modified) - for (VirtualHost host : getVirtualHosts()) - for (String alias : host.getAliases()) - hosts.put(alias, host); - // start handling incoming connections - new SocketHandlerThread().start(); - } - - /** - * Stops this server. If it is already stopped, does nothing. - * Note that if an {@link #setExecutor Executor} was set, it must be closed separately. - */ - public synchronized void stop() { - try { - if (serv != null) - serv.close(); - } catch (IOException ignore) {} - serv = null; - } - - /** - * Handles communications for a single connection over the given streams. - * Multiple subsequent transactions are handled on the connection, - * until the streams are closed, an error occurs, or the request - * contains a "Connection: close" header which explicitly requests - * the connection be closed after the transaction ends. - * - * @param in the stream from which the incoming requests are read - * @param out the stream into which the outgoing responses are written - * @throws IOException if an error occurs - */ - protected void handleConnection(InputStream in, OutputStream out) throws IOException { - in = new BufferedInputStream(in, 4096); - out = new BufferedOutputStream(out, 4096); - Request req; - Response resp; - do { - // create request and response and handle transaction - req = null; - resp = new Response(out); - try { - req = new Request(in); - handleTransaction(req, resp); - } catch (Throwable t) { // unhandled errors (not normal error responses like 404) - if (req == null) { // error reading request - if (t instanceof IOException && t.getMessage().contains("missing request line")) - break; // we're not in the middle of a transaction - so just disconnect - resp.getHeaders().add("Connection", "close"); // about to close connection - if (t instanceof InterruptedIOException) // e.g. SocketTimeoutException - resp.sendError(408, "Timeout waiting for client request"); - else - resp.sendError(400, "Invalid request: " + t.getMessage()); - } else if (!resp.headersSent()) { // if headers were not already sent, we can send an error response - resp = new Response(out); // ignore whatever headers may have already been set - resp.getHeaders().add("Connection", "close"); // about to close connection - resp.sendError(500, "Error processing request: " + t.getMessage()); - } // otherwise just abort the connection since we can't recover - break; // proceed to close connection - } finally { - resp.close(); // close response and flush output - } - // consume any leftover body data so next request can be processed - transfer(req.getBody(), null, -1); - // RFC7230#6.6: persist connection unless client or server close explicitly (or legacy client) - } while (!"close".equalsIgnoreCase(req.getHeaders().get("Connection")) - && !"close".equalsIgnoreCase(resp.getHeaders().get("Connection")) && req.getVersion().endsWith("1.1")); - } - - /** - * Handles a single transaction on a connection. - * - * Subclasses can override this method to perform filtering on the - * request or response, apply wrappers to them, or further customize - * the transaction processing in some other way. - * - * @param req the transaction request - * @param resp the transaction response (into which the response is written) - * @throws IOException if and error occurs - */ - protected void handleTransaction(Request req, Response resp) throws IOException { - resp.setClientCapabilities(req); - if (preprocessTransaction(req, resp)) - handleMethod(req, resp); - } - - /** - * Preprocesses a transaction, performing various validation checks - * and required special header handling, possibly returning an - * appropriate response. - * - * @param req the request - * @param resp the response - * @return whether further processing should be performed on the transaction - * @throws IOException if an error occurs - */ - protected boolean preprocessTransaction(Request req, Response resp) throws IOException { - Headers reqHeaders = req.getHeaders(); - // validate request - String version = req.getVersion(); - if (version.equals("HTTP/1.1")) { - if (!reqHeaders.contains("Host")) { - // RFC2616#14.23: missing Host header gets 400 - resp.sendError(400, "Missing required Host header"); - return false; - } - // return a continue response before reading body - String expect = reqHeaders.get("Expect"); - if (expect != null) { - if (expect.equalsIgnoreCase("100-continue")) { - Response tempResp = new Response(resp.getOutputStream()); - tempResp.sendHeaders(100); - resp.getOutputStream().flush(); - } else { - // RFC2616#14.20: if unknown expect, send 417 - resp.sendError(417); - return false; - } - } - } else if (version.equals("HTTP/1.0") || version.equals("HTTP/0.9")) { - // RFC2616#14.10 - remove connection headers from older versions - for (String token : splitElements(reqHeaders.get("Connection"), false)) - reqHeaders.remove(token); - } else { - resp.sendError(400, "Unknown version: " + version); - return false; - } - return true; - } - - /** - * Handles a transaction according to the request method. - * - * @param req the transaction request - * @param resp the transaction response (into which the response is written) - * @throws IOException if and error occurs - */ - protected void handleMethod(Request req, Response resp) throws IOException { - String method = req.getMethod(); - Map handlers = req.getContext().getHandlers(); - // RFC 2616#5.1.1 - GET and HEAD must be supported - if (method.equals("GET") || handlers.containsKey(method)) { - serve(req, resp); // method is handled by context handler (or 404) - } else if (method.equals("HEAD")) { // default HEAD handler - req.method = "GET"; // identical to a GET - resp.setDiscardBody(true); // process normally but discard body - serve(req, resp); - } else if (method.equals("TRACE")) { // default TRACE handler - handleTrace(req, resp); - } else { - Set methods = new LinkedHashSet(); - methods.addAll(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); // built-in methods - // "*" is a special server-wide (no-context) request supported by OPTIONS - boolean isServerOptions = req.getPath().equals("*") && method.equals("OPTIONS"); - methods.addAll(isServerOptions ? req.getVirtualHost().getMethods() : handlers.keySet()); - resp.getHeaders().add("Allow", join(", ", methods)); - if (method.equals("OPTIONS")) { // default OPTIONS handler - resp.getHeaders().add("Content-Length", "0"); // RFC2616#9.2 - resp.sendHeaders(200); - } else if (req.getVirtualHost().getMethods().contains(method)) { - resp.sendHeaders(405); // supported by server, but not this context (nor built-in) - } else { - resp.sendError(501); // unsupported method - } - } - } - - /** - * Handles a TRACE method request. - * - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - public void handleTrace(Request req, Response resp) throws IOException { - resp.sendHeaders(200, -1, -1, null, "message/http", null); - OutputStream out = resp.getBody(); - out.write(getBytes("TRACE ", req.getURI().toString(), " ", req.getVersion())); - out.write(CRLF); - req.getHeaders().writeTo(out); - transfer(req.getBody(), out, -1); - } - - /** - * Serves the content for a request by invoking the context - * handler for the requested context (path) and HTTP method. - * - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - protected void serve(Request req, Response resp) throws IOException { - // get context handler to handle request - ContextHandler handler = req.getContext().getHandlers().get(req.getMethod()); - if (handler == null) { - resp.sendError(404); - return; - } - // serve request - int status = 404; - // add directory index if necessary - String path = req.getPath(); - if (path.endsWith("/")) { - String index = req.getVirtualHost().getDirectoryIndex(); - if (index != null) { - req.setPath(path + index); - status = handler.serve(req, resp); - req.setPath(path); - } - } - if (status == 404) - status = handler.serve(req, resp); - if (status > 0) - resp.sendError(status); - } - - /** - * Adds a Content-Type mapping for the given path suffixes. - * If any of the path suffixes had a previous Content-Type associated - * with it, it is replaced with the given one. Path suffixes are - * considered case-insensitive, and contentType is converted to lowercase. - * - * @param contentType the content type (MIME type) to be associated with - * the given path suffixes - * @param suffixes the path suffixes which will be associated with - * the contentType, e.g. the file extensions of served files - * (excluding the '.' character) - */ - public static void addContentType(String contentType, String... suffixes) { - for (String suffix : suffixes) - contentTypes.put(suffix.toLowerCase(Locale.US), contentType.toLowerCase(Locale.US)); - } - - /** - * Adds Content-Type mappings from a standard mime.types file. - * - * @param mimeTypes a mime.types file - * @throws IOException if an error occurs - * @throws FileNotFoundException if the file is not found or cannot be read - */ - public static void addContentTypes(File mimeTypes) throws IOException { - InputStream in = new FileInputStream(mimeTypes); - try { - while (true) { - String line = readLine(in).trim(); // throws EOFException when done - if (line.length() > 0 && line.charAt(0) != '#') { - String[] tokens = split(line, " \t", -1); - for (int i = 1; i < tokens.length; i++) - addContentType(tokens[0], tokens[i]); - } - } - } catch (EOFException ignore) { // the end of file was reached - it's ok - } finally { - in.close(); - } - } - - /** - * Returns the content type for the given path, according to its suffix, - * or the given default content type if none can be determined. - * - * @param path the path whose content type is requested - * @param def a default content type which is returned if none can be - * determined - * @return the content type for the given path, or the given default - */ - public static String getContentType(String path, String def) { - int dot = path.lastIndexOf('.'); - String type = dot < 0 ? def : contentTypes.get(path.substring(dot + 1).toLowerCase(Locale.US)); - return type != null ? type : def; - } - - /** - * Checks whether data of the given content type (MIME type) is compressible. - * - * @param contentType the content type - * @return true if the data is compressible, false if not - */ - public static boolean isCompressible(String contentType) { - int pos = contentType.indexOf(';'); // exclude params - String ct = pos < 0 ? contentType : contentType.substring(0, pos); - for (String s : compressibleContentTypes) - if (s.equals(ct) || s.charAt(0) == '*' && ct.endsWith(s.substring(1)) - || s.charAt(s.length() - 1) == '*' && ct.startsWith(s.substring(0, s.length() - 1))) - return true; - return false; - } - - /** - * Returns the local host's auto-detected name. - * - * @return the local host name - */ - public static String detectLocalHostName() { - try { - return InetAddress.getLocalHost().getCanonicalHostName(); - } catch (UnknownHostException uhe) { - return "localhost"; - } - } - - /** - * Parses name-value pair parameters from the given "x-www-form-urlencoded" - * MIME-type string. This is the encoding used both for parameters passed - * as the query of an HTTP GET method, and as the content of HTML forms - * submitted using the HTTP POST method (as long as they use the default - * "application/x-www-form-urlencoded" encoding in their ENCTYPE attribute). - * UTF-8 encoding is assumed. - *

- * The parameters are returned as a list of string arrays, each containing - * the parameter name as the first element and its corresponding value - * as the second element (or an empty string if there is no value). - *

- * The list retains the original order of the parameters. - * - * @param s an "application/x-www-form-urlencoded" string - * @return the parameter name-value pairs parsed from the given string, - * or an empty list if there are none - */ - public static List parseParamsList(String s) { - if (s == null || s.length() == 0) - return Collections.emptyList(); - List params = new ArrayList(8); - for (String pair : split(s, "&", -1)) { - int pos = pair.indexOf('='); - String name = pos < 0 ? pair : pair.substring(0, pos); - String val = pos < 0 ? "" : pair.substring(pos + 1); - try { - name = URLDecoder.decode(name.trim(), "UTF-8"); - val = URLDecoder.decode(val.trim(), "UTF-8"); - if (name.length() > 0) - params.add(new String[] { name, val }); - } catch (UnsupportedEncodingException ignore) {} // never thrown - } - return params; - } - - /** - * Converts a collection of pairs of objects (arrays of size two, - * each representing a key and corresponding value) into a Map. - * Duplicate keys are ignored (only the first occurrence of each key is considered). - * The map retains the original collection's iteration order. - * - * @param pairs a collection of arrays, each containing a key and corresponding value - * @param the key type - * @param the value type - * @return a map containing the paired keys and values, or an empty map - */ - @SuppressWarnings("unchecked") - public static Map toMap(Collection pairs) { - if (pairs == null || pairs.isEmpty()) - return Collections.emptyMap(); - Map map = new LinkedHashMap(pairs.size()); - for (Object[] pair : pairs) - if (!map.containsKey(pair[0])) - map.put((K)pair[0], (V)pair[1]); - return map; - } - - /** - * Returns the absolute (zero-based) content range value specified - * by the given range string. If multiple ranges are requested, a single - * range containing all of them is returned. - * - * @param range the string containing the range description - * @param length the full length of the requested resource - * @return the requested range, or null if the range value is invalid - */ - public static long[] parseRange(String range, long length) { - long min = Long.MAX_VALUE; - long max = Long.MIN_VALUE; - try { - for (String token : splitElements(range, false)) { - long start, end; - int dash = token.indexOf('-'); - if (dash == 0) { // suffix range - start = length - parseULong(token.substring(1), 10); - end = length - 1; - } else if (dash == token.length() - 1) { // open range - start = parseULong(token.substring(0, dash), 10); - end = length - 1; - } else { // explicit range - start = parseULong(token.substring(0, dash), 10); - end = parseULong(token.substring(dash + 1), 10); - } - if (end < start) - throw new RuntimeException(); - if (start < min) - min = start; - if (end > max) - max = end; - } - if (max < 0) // no tokens - throw new RuntimeException(); - if (max >= length && min < length) - max = length - 1; - return new long[] { min, max }; // start might be >= length! - } catch (RuntimeException re) { // NFE, IOOBE or explicit RE - return null; // RFC2616#14.35.1 - ignore header if invalid - } - } - - /** - * Parses an unsigned long value. This method behaves the same as calling - * {@link Long#parseLong(String, int)}, but considers the string invalid - * if it starts with an ASCII minus sign ('-') or plus sign ('+'). - * - * @param s the String containing the long representation to be parsed - * @param radix the radix to be used while parsing s - * @return the long represented by s in the specified radix - * @throws NumberFormatException if the string does not contain a parsable - * long, or if it starts with an ASCII minus sign or plus sign - */ - public static long parseULong(String s, int radix) throws NumberFormatException { - long val = Long.parseLong(s, radix); // throws NumberFormatException - if (s.charAt(0) == '-' || s.charAt(0) == '+') - throw new NumberFormatException("invalid digit: " + s.charAt(0)); - return val; - } - - /** - * Parses a date string in one of the supported {@link #DATE_PATTERNS}. - * - * Received date header values must be in one of the following formats: - * Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 - * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 - * Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format - * - * @param time a string representation of a time value - * @return the parsed date value - * @throws IllegalArgumentException if the given string does not contain - * a valid date format in any of the supported formats - */ - public static Date parseDate(String time) { - for (String pattern : DATE_PATTERNS) { - try { - SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.US); - df.setLenient(false); - df.setTimeZone(GMT); - return df.parse(time); - } catch (ParseException ignore) {} - } - throw new IllegalArgumentException("invalid date format: " + time); - } - - /** - * Formats the given time value as a string in RFC 1123 format. - * - * @param time the time in milliseconds since January 1, 1970, 00:00:00 GMT - * @return the given time value as a string in RFC 1123 format - */ - public static String formatDate(long time) { - // this implementation performs far better than SimpleDateFormat instances, and even - // quite better than ThreadLocal SDFs - the server's CPU-bound benchmark gains over 20%! - if (time < -62167392000000L || time > 253402300799999L) - throw new IllegalArgumentException("year out of range (0001-9999): " + time); - char[] s = "DAY, 00 MON 0000 00:00:00 GMT".toCharArray(); // copy the format template - Calendar cal = new GregorianCalendar(GMT, Locale.US); - cal.setTimeInMillis(time); - System.arraycopy(DAYS, 4 * (cal.get(Calendar.DAY_OF_WEEK) - 1), s, 0, 3); - System.arraycopy(MONTHS, 4 * cal.get(Calendar.MONTH), s, 8, 3); - int n = cal.get(Calendar.DATE); s[5] += n / 10; s[6] += n % 10; - n = cal.get(Calendar.YEAR); s[12] += n / 1000; s[13] += n / 100 % 10; - s[14] += n / 10 % 10; s[15] += n % 10; - n = cal.get(Calendar.HOUR_OF_DAY); s[17] += n / 10; s[18] += n % 10; - n = cal.get(Calendar.MINUTE); s[20] += n / 10; s[21] += n % 10; - n = cal.get(Calendar.SECOND); s[23] += n / 10; s[24] += n % 10; - return new String(s); - } - - /** - * Splits the given element list string (comma-separated header value) - * into its constituent non-empty trimmed elements. - * (RFC2616#2.1: element lists are delimited by a comma and optional LWS, - * and empty elements are ignored). - * - * @param list the element list string - * @param lower specifies whether the list elements should be lower-cased - * @return the non-empty elements in the list, or an empty array - */ - public static String[] splitElements(String list, boolean lower) { - return split(lower && list != null ? list.toLowerCase(Locale.US) : list, ",", -1); - } - - /** - * Splits the given string into its constituent non-empty trimmed elements, - * which are delimited by any of the given delimiter characters. - * This is a more direct and efficient implementation than using a regex - * (e.g. String.split()), trimming the elements and removing empty ones. - * - * @param str the string to split - * @param delimiters the characters used as the delimiters between elements - * @param limit if positive, limits the returned array size (remaining of str in last element) - * @return the non-empty elements in the string, or an empty array - */ - public static String[] split(String str, String delimiters, int limit) { - if (str == null) - return new String[0]; - Collection elements = new ArrayList(); - int len = str.length(); - int start = 0; - int end; - while (start < len) { - for (end = --limit == 0 ? len : start; - end < len && delimiters.indexOf(str.charAt(end)) < 0; end++); - String element = str.substring(start, end).trim(); - if (element.length() > 0) - elements.add(element); - start = end + 1; - } - return elements.toArray(new String[elements.size()]); - } - - /** - * Returns a string constructed by joining the string representations of the - * iterated objects (in order), with the delimiter inserted between them. - * - * @param delim the delimiter that is inserted between the joined strings - * @param items the items whose string representations are joined - * @param the item type - * @return the joined string - */ - public static String join(String delim, Iterable items) { - StringBuilder sb = new StringBuilder(); - for (Iterator it = items.iterator(); it.hasNext(); ) - sb.append(it.next()).append(it.hasNext() ? delim : ""); - return sb.toString(); - } - - /** - * Returns the parent of the given path. - * - * @param path the path whose parent is returned (must start with '/') - * @return the parent of the given path (excluding trailing slash), - * or null if given path is the root path - */ - public static String getParentPath(String path) { - path = trimRight(path, '/'); // remove trailing slash - int slash = path.lastIndexOf('/'); - return slash < 0 ? null : path.substring(0, slash); - } - - /** - * Returns the given string with all occurrences of the given character - * removed from its right side. - * - * @param s the string to trim - * @param c the character to remove - * @return the trimmed string - */ - public static String trimRight(String s, char c) { - int len = s.length() - 1; - int end; - for (end = len; end >= 0 && s.charAt(end) == c; end--); - return end == len ? s : s.substring(0, end + 1); - } - - /** - * Returns the given string with all occurrences of the given character - * removed from its left side. - * - * @param s the string to trim - * @param c the character to remove - * @return the trimmed string - */ - public static String trimLeft(String s, char c) { - int len = s.length(); - int start; - for (start = 0; start < len && s.charAt(start) == c; start++); - return start == 0 ? s : s.substring(start); - } - - /** - * Trims duplicate consecutive occurrences of the given character within the - * given string, replacing them with a single instance of the character. - * - * @param s the string to trim - * @param c the character to trim - * @return the given string with duplicate consecutive occurrences of c - * replaced by a single instance of c - */ - public static String trimDuplicates(String s, char c) { - int start = 0; - while ((start = s.indexOf(c, start) + 1) > 0) { - int end; - for (end = start; end < s.length() && s.charAt(end) == c; end++); - if (end > start) - s = s.substring(0, start) + s.substring(end); - } - return s; - } - - /** - * Returns a human-friendly string approximating the given data size, - * e.g. "316", "1.8K", "324M", etc. - * - * @param size the size to display - * @return a human-friendly string approximating the given data size - */ - public static String toSizeApproxString(long size) { - final char[] units = { ' ', 'K', 'M', 'G', 'T', 'P', 'E' }; - int u; - double s; - for (u = 0, s = size; s >= 1000; u++, s /= 1024); - return String.format(s < 10 ? "%.1f%c" : "%.0f%c", s, units[u]); - } - - /** - * Returns an HTML-escaped version of the given string for safe display - * within a web page. The characters '&', '>' and '<' must always - * be escaped, and single and double quotes must be escaped within - * attribute values; this method escapes them always. This method can - * be used for generating both HTML and XHTML valid content. - * - * @param s the string to escape - * @return the escaped string - * @see The W3C FAQ - */ - public static String escapeHTML(String s) { - int len = s.length(); - StringBuilder sb = new StringBuilder(len + 30); - int start = 0; - for (int i = 0; i < len; i++) { - String ref = null; - switch (s.charAt(i)) { - case '&': ref = "&"; break; - case '>': ref = ">"; break; - case '<': ref = "<"; break; - case '"': ref = """; break; - case '\'': ref = "'"; break; - } - if (ref != null) { - sb.append(s.substring(start, i)).append(ref); - start = i + 1; - } - } - return start == 0 ? s : sb.append(s.substring(start)).toString(); - } - - /** - * Converts strings to bytes by casting the chars to bytes. - * This is a fast way to encode a string as ISO-8859-1/US-ASCII bytes. - * If multiple strings are provided, their bytes are concatenated. - * - * @param strings the strings to convert (containing only ISO-8859-1 chars) - * @return the byte array - */ - public static byte[] getBytes(String... strings) { - int n = 0; - for (String s : strings) - n += s.length(); - byte[] dest = new byte[n]; - n = 0; - for (String s : strings) - for (int i = 0, len = s.length(); i < len; i++) - dest[n++] = (byte)s.charAt(i); - return dest; - } - - /** - * Transfers data from an input stream to an output stream. - * - * @param in the input stream to transfer from - * @param out the output stream to transfer to (or null to discard output) - * @param len the number of bytes to transfer. If negative, the entire - * contents of the input stream are transferred. - * @throws IOException if an IO error occurs or the input stream ends - * before the requested number of bytes have been read - */ - public static void transfer(InputStream in, OutputStream out, long len) throws IOException { - if (len == 0 || out == null && len < 0 && in.read() < 0) - return; // small optimization - avoid buffer creation - byte[] buf = new byte[4096]; - while (len != 0) { - int count = len < 0 || buf.length < len ? buf.length : (int)len; - count = in.read(buf, 0, count); - if (count < 0) { - if (len > 0) - throw new IOException("unexpected end of stream"); - break; - } - if (out != null) - out.write(buf, 0, count); - len -= len > 0 ? count : 0; - } - } - - /** - * Reads the token starting at the current stream position and ending at - * the first occurrence of the given delimiter byte, in the given encoding. - * - * @param in the stream from which the token is read - * @param delim the byte value which marks the end of the token, - * or -1 if the token ends at the end of the stream - * @param enc a character-encoding name - * @param maxLength the maximum length (in bytes) to read - * @return the read token, excluding the delimiter - * @throws UnsupportedEncodingException if the encoding is not supported - * @throws EOFException if the stream end is reached before a delimiter is found - * @throws IOException if an IO error occurs, or the maximum length - * is reached before the token end is reached - */ - public static String readToken(InputStream in, int delim, - String enc, int maxLength) throws IOException { - // note: we avoid using a ByteArrayOutputStream here because it - // suffers the overhead of synchronization for each byte written - int buflen = maxLength < 512 ? maxLength : 512; // start with less - byte[] buf = new byte[buflen]; - int count = 0; - int c; - while ((c = in.read()) != -1 && c != delim) { - if (count == buflen) { // expand buffer - if (count == maxLength) - throw new IOException("token too large (" + count + ")"); - buflen = maxLength < 2 * buflen ? maxLength : 2 * buflen; - byte[] expanded = new byte[buflen]; - System.arraycopy(buf, 0, expanded, 0, count); - buf = expanded; - } - buf[count++] = (byte)c; - } - if (c < 0 && delim != -1) - throw new EOFException("unexpected end of stream"); - return new String(buf, 0, count, enc); - } - - /** - * Reads the ISO-8859-1 encoded string starting at the current stream - * position and ending at the first occurrence of the LF character. - * - * @param in the stream from which the line is read - * @return the read string, excluding the terminating LF character - * and (if exists) the CR character immediately preceding it - * @throws EOFException if the stream end is reached before an LF character is found - * @throws IOException if an IO error occurs, or the line is longer than 8192 bytes - * @see #readToken(InputStream, int, String, int) - */ - public static String readLine(InputStream in) throws IOException { - String s = readToken(in, '\n', "ISO8859_1", 8192); - return s.length() > 0 && s.charAt(s.length() - 1) == '\r' - ? s.substring(0, s.length() - 1) : s; - } - - /** - * Reads headers from the given stream. Headers are read according to the - * RFC, including folded headers, element lists, and multiple headers - * (which are concatenated into a single element list header). - * Leading and trailing whitespace is removed. - * - * @param in the stream from which the headers are read - * @return the read headers (possibly empty, if none exist) - * @throws IOException if an IO error occurs or the headers are malformed - * or there are more than 100 header lines - */ - public static Headers readHeaders(InputStream in) throws IOException { - Headers headers = new Headers(); - String line; - String prevLine = ""; - int count = 0; - while ((line = readLine(in)).length() > 0) { - int first; - for (first = 0; first < line.length() && - Character.isWhitespace(line.charAt(first)); first++); - if (first > 0) // unfold header continuation line - line = prevLine + ' ' + line.substring(first); - int separator = line.indexOf(':'); - if (separator < 0) - throw new IOException("invalid header: \"" + line + "\""); - String name = line.substring(0, separator); - String value = line.substring(separator + 1).trim(); // ignore LWS - Header replaced = headers.replace(name, value); - // concatenate repeated headers (distinguishing repeated from folded) - if (replaced != null && first == 0) { - value = replaced.getValue() + ", " + value; - line = name + ": " + value; - headers.replace(name, value); - } - prevLine = line; - if (++count > 100) - throw new IOException("too many header lines"); - } - return headers; - } - - /** - * Matches the given ETag value against the given ETags. A match is found - * if the given ETag is not null, and either the ETags contain a "*" value, - * or one of them is identical to the given ETag. If strong comparison is - * used, tags beginning with the weak ETag prefix "W/" never match. - * See RFC2616#3.11, RFC2616#13.3.3. - * - * @param strong if true, strong comparison is used, otherwise weak - * comparison is used - * @param etags the ETags to match against - * @param etag the ETag to match - * @return true if the ETag is matched, false otherwise - */ - public static boolean match(boolean strong, String[] etags, String etag) { - if (etag == null || strong && etag.startsWith("W/")) - return false; - for (String e : etags) - if (e.equals("*") || (e.equals(etag) && !(strong && (e.startsWith("W/"))))) - return true; - return false; - } - - /** - * Calculates the appropriate response status for the given request and - * its resource's last-modified time and ETag, based on the conditional - * headers present in the request. - * - * @param req the request - * @param lastModified the resource's last modified time - * @param etag the resource's ETag - * @return the appropriate response status for the request - */ - public static int getConditionalStatus(Request req, long lastModified, String etag) { - Headers headers = req.getHeaders(); - // If-Match - String header = headers.get("If-Match"); - if (header != null && !match(true, splitElements(header, false), etag)) - return 412; - // If-Unmodified-Since - Date date = headers.getDate("If-Unmodified-Since"); - if (date != null && lastModified > date.getTime()) - return 412; - // If-Modified-Since - int status = 200; - boolean force = false; - date = headers.getDate("If-Modified-Since"); - if (date != null && date.getTime() <= System.currentTimeMillis()) { - if (lastModified > date.getTime()) - force = true; - else - status = 304; - } - // If-None-Match - header = headers.get("If-None-Match"); - if (header != null) { - if (match(false, splitElements(header, false), etag)) // RFC7232#3.2: use weak matching - status = req.getMethod().equals("GET") - || req.getMethod().equals("HEAD") ? 304 : 412; - else - force = true; - } - return force ? 200 : status; - } - - /** - * Serves a context's contents from a file based resource. - * - * The file is located by stripping the given context prefix from - * the request's path, and appending the result to the given base directory. - * - * Missing, forbidden and otherwise invalid files return the appropriate - * error response. Directories are served as an HTML index page if the - * virtual host allows one, or a forbidden error otherwise. Files are - * sent with their corresponding content types, and handle conditional - * and partial retrievals according to the RFC. - * - * @param base the base directory to which the context is mapped - * @param context the context which is mapped to the base directory - * @param req the request - * @param resp the response into which the content is written - * @return the HTTP status code to return, or 0 if a response was sent - * @throws IOException if an error occurs - */ - public static int serveFile(File base, String context, - Request req, Response resp) throws IOException { - String relativePath = req.getPath().substring(context.length()); - File file = new File(base, relativePath).getCanonicalFile(); - if (!file.exists() || file.isHidden() || file.getName().startsWith(".")) { - return 404; - } else if (!file.canRead() || !file.getPath().startsWith(base.getPath())) { // validate - return 403; - } else if (file.isDirectory()) { - if (relativePath.endsWith("/")) { - if (!req.getVirtualHost().isAllowGeneratedIndex()) - return 403; - resp.send(200, createIndex(file, req.getPath())); - } else { // redirect to the normalized directory URL ending with '/' - resp.redirect(req.getBaseURL() + req.getPath() + "/", true); - } - } else if (relativePath.endsWith("/")) { - return 404; // non-directory ending with slash (File constructor removed it) - } else { - serveFileContent(file, req, resp); - } - return 0; - } - - /** - * Serves the contents of a file, with its corresponding content type, - * last modification time, etc. conditional and partial retrievals are - * handled according to the RFC. - * - * @param file the existing and readable file whose contents are served - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - public static void serveFileContent(File file, Request req, Response resp) throws IOException { - long len = file.length(); - long lastModified = file.lastModified(); - String etag = "W/\"" + lastModified + "\""; // a weak tag based on date - int status = 200; - // handle range or conditional request - long[] range = req.getRange(len); - if (range == null || len == 0) { - status = getConditionalStatus(req, lastModified, etag); - } else { - String ifRange = req.getHeaders().get("If-Range"); - if (ifRange == null) { - if (range[0] >= len) - status = 416; // unsatisfiable range - else - status = getConditionalStatus(req, lastModified, etag); - } else if (range[0] >= len) { - // RFC2616#14.16, 10.4.17: invalid If-Range gets everything - range = null; - } else { // send either range or everything - if (!ifRange.startsWith("\"") && !ifRange.startsWith("W/")) { - Date date = req.getHeaders().getDate("If-Range"); - if (date != null && lastModified > date.getTime()) - range = null; // modified - send everything - } else if (!ifRange.equals(etag)) { - range = null; // modified - send everything - } - } - } - // send the response - Headers respHeaders = resp.getHeaders(); - switch (status) { - case 304: // no other headers or body allowed - respHeaders.add("ETag", etag); - respHeaders.add("Vary", "Accept-Encoding"); - respHeaders.add("Last-Modified", formatDate(lastModified)); - resp.sendHeaders(304); - break; - case 412: - resp.sendHeaders(412); - break; - case 416: - respHeaders.add("Content-Range", "bytes */" + len); - resp.sendHeaders(416); - break; - case 200: - // send OK response - resp.sendHeaders(200, len, lastModified, etag, - getContentType(file.getName(), "application/octet-stream"), range); - // send body - InputStream in = new FileInputStream(file); - try { - resp.sendBody(in, len, range); - } finally { - in.close(); - } - break; - default: - resp.sendHeaders(500); // should never happen - break; - } - } - - /** - * Serves the contents of a directory as an HTML file index. - * - * @param dir the existing and readable directory whose contents are served - * @param path the displayed base path corresponding to dir - * @return an HTML string containing the file index for the directory - */ - public static String createIndex(File dir, String path) { - if (!path.endsWith("/")) - path += "/"; - // calculate name column width - int w = 21; // minimum width - for (String name : dir.list()) - if (name.length() > w) - w = name.length(); - w += 2; // with room for added slash and space - // note: we use apache's format, for consistent user experience - Formatter f = new Formatter(Locale.US); - f.format("%n" + - "Index of %s%n" + - "

Index of %s

%n" + - "
 Name%" + (w - 5) + "s Last modified      Size
", - path, path, ""); - if (path.length() > 1) // add parent link if not root path - f.format(" Parent Directory%" - + (w + 5) + "s-%n", getParentPath(path), ""); - for (File file : dir.listFiles()) { - try { - String name = file.getName() + (file.isDirectory() ? "/" : ""); - String size = file.isDirectory() ? "- " : toSizeApproxString(file.length()); - // properly url-encode the link - String link = new URI(null, path + name, null).toASCIIString(); - if (!file.isHidden() && !name.startsWith(".")) - f.format(" %s%-" + (w - name.length()) + - "s‎%td-%"); - return f.toString(); - } - - /** - * Starts a stand-alone HTTP server, serving files from disk. - * - * @param args the command line arguments - */ - // public static void main(String[] args) { - // try { - // if (args.length == 0) { - // System.err.printf("Usage: java [-options] %s [port]%n" + - // "To enable SSL: specify options -Djavax.net.ssl.keyStore, " + - // "-Djavax.net.ssl.keyStorePassword, etc.%n", HTTPServer.class.getName()); - // return; - // } - // File dir = new File(args[0]); - // if (!dir.canRead()) - // throw new FileNotFoundException(dir.getAbsolutePath()); - // int port = args.length < 2 ? 80 : Integer.parseInt(args[1]); - // // set up server - // for (File f : Arrays.asList(new File("/etc/mime.types"), new File(dir, ".mime.types"))) - // if (f.exists()) - // addContentTypes(f); - // HTTPServer server = new HTTPServer(port); - // if (System.getProperty("javax.net.ssl.keyStore") != null) // enable SSL if configured - // server.setServerSocketFactory(SSLServerSocketFactory.getDefault()); - // VirtualHost host = server.getVirtualHost(null); // default host - // host.setAllowGeneratedIndex(true); // with directory index pages - // host.addContext("/", new FileContextHandler(dir)); - // host.addContext("/api/time", new ContextHandler() { - // public int serve(Request req, Response resp) throws IOException { - // long now = System.currentTimeMillis(); - // resp.getHeaders().add("Content-Type", "text/plain"); - // resp.send(200, String.format("%tF %. - * - * For additional info see http://www.freeutils.net/source/jlhttp/ - */ - -import javax.net.ServerSocketFactory; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.SSLSocket; -import java.io.*; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.*; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.*; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.GZIPOutputStream; - -/** - * The {@code HTTPServer} class implements a light-weight HTTP server. - *

- * This server implements all functionality required by RFC 2616 ("Hypertext - * Transfer Protocol -- HTTP/1.1"), as well as some of the optional - * functionality (this is termed "conditionally compliant" in the RFC). - * In fact, a couple of bugs in the RFC itself were discovered - * (and fixed) during the development of this server. - *

- * Feature Overview - *

    - *
  • RFC compliant - correctness is not sacrificed for the sake of size
  • - *
  • Virtual hosts - multiple domains and subdomains per server
  • - *
  • File serving - built-in handler to serve files and folders from disk
  • - *
  • Mime type mappings - configurable via API or a standard mime.types file
  • - *
  • Directory index generation - enables browsing folder contents
  • - *
  • Welcome files - configurable default filename (e.g. index.html)
  • - *
  • All HTTP methods supported - GET/HEAD/OPTIONS/TRACE/POST/PUT/DELETE/custom
  • - *
  • Conditional statuses - ETags and If-* header support
  • - *
  • Chunked transfer encoding - for serving dynamically-generated data streams
  • - *
  • Gzip/deflate compression - reduces bandwidth and download time
  • - *
  • HTTPS - secures all server communications
  • - *
  • Partial content - download continuation (a.k.a. byte range serving)
  • - *
  • File upload - multipart/form-data handling as stream or iterator
  • - *
  • Multiple context handlers - a different handler method per URL path
  • - *
  • @Context annotations - auto-detection of context handler methods
  • - *
  • Parameter parsing - from query string or x-www-form-urlencoded body
  • - *
  • A single source file - super-easy to integrate into any application
  • - *
  • Standalone - no dependencies other than the Java runtime
  • - *
  • Small footprint - standard jar is ~50K, stripped jar is ~35K
  • - *
  • Extensible design - easy to override, add or remove functionality
  • - *
  • Reusable utility methods to simplify your custom code
  • - *
  • Extensive documentation of API and implementation (>40% of source lines)
  • - *
- *

- * Use Cases - *

- * Being a lightweight, standalone, easily embeddable and tiny-footprint - * server, it is well-suited for - *

    - *
  • Resource-constrained environments such as embedded devices. - * For really extreme constraints, you can easily remove unneeded - * functionality to make it even smaller (and use the -Dstripped - * maven build option to strip away debug info, license, etc.)
  • - *
  • Unit and integration tests - fast setup/teardown times, small overhead - * and simple context handler setup make it a great web server for testing - * client components under various server response conditions.
  • - *
  • Embedding a web console into any headless application for - * administration, monitoring, or a full portable GUI.
  • - *
  • A full-fledged standalone web server serving static files, - * dynamically-generated content, REST APIs, pseudo-streaming, etc.
  • - *
  • A good reference for learning how HTTP works under the hood.
  • - *
- *

- * Implementation Notes - *

- * The design and implementation of this server attempt to balance correctness, - * compliance, readability, size, features, extensibility and performance, - * and often prioritize them in this order, but some trade-offs must be made. - *

- * This server is multithreaded in its support for multiple concurrent HTTP - * connections, however most of its constituent classes are not thread-safe and - * require external synchronization if accessed by multiple threads concurrently. - *

- * Source Structure and Documentation - *

- * This server is intentionally written as a single source file, in order to make - * it as easy as possible to integrate into any existing project - by simply adding - * this single file to the project sources. It does, however, aim to maintain a - * structured and flexible design. There are no external package dependencies. - *

- * This file contains extensive documentation of its classes and methods, as - * well as implementation details and references to specific RFC sections - * which clarify the logic behind the code. It is recommended that anyone - * attempting to modify the protocol-level functionality become acquainted with - * the RFC, in order to make sure that protocol compliance is not broken. - *

- * Getting Started - *

- * For an example and a good starting point for learning how to use the API, - * see the {@link #main main} method at the bottom of the file, and follow - * the code into the API from there. Alternatively, you can just browse through - * the classes and utility methods and read their documentation and code. - * - * @author Amichai Rothman - * @since 2008-07-24 - */ -public class HTTPServer { - - /** - * The SimpleDateFormat-compatible formats of dates which must be supported. - * Note that all generated date fields must be in the RFC 1123 format only, - * while the others are supported by recipients for backwards-compatibility. - */ - public static final String[] DATE_PATTERNS = { - "EEE, dd MMM yyyy HH:mm:ss z", // RFC 822, updated by RFC 1123 - "EEEE, dd-MMM-yy HH:mm:ss z", // RFC 850, obsoleted by RFC 1036 - "EEE MMM d HH:mm:ss yyyy" // ANSI C's asctime() format - }; - - /** A GMT (UTC) timezone instance. */ - protected static final TimeZone GMT = TimeZone.getTimeZone("GMT"); - - /** Date format strings. */ - protected static final char[] - DAYS = "Sun Mon Tue Wed Thu Fri Sat".toCharArray(), - MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".toCharArray(); - - /** A convenience array containing the carriage-return and line feed chars. */ - public static final byte[] CRLF = { 0x0d, 0x0a }; - - /** The HTTP status description strings. */ - protected static final String[] statuses = new String[600]; - - static { - // initialize status descriptions lookup table - Arrays.fill(statuses, "Unknown Status"); - statuses[100] = "Continue"; - statuses[200] = "OK"; - statuses[204] = "No Content"; - statuses[206] = "Partial Content"; - statuses[301] = "Moved Permanently"; - statuses[302] = "Found"; - statuses[304] = "Not Modified"; - statuses[307] = "Temporary Redirect"; - statuses[400] = "Bad Request"; - statuses[401] = "Unauthorized"; - statuses[403] = "Forbidden"; - statuses[404] = "Not Found"; - statuses[405] = "Method Not Allowed"; - statuses[408] = "Request Timeout"; - statuses[412] = "Precondition Failed"; - statuses[413] = "Request Entity Too Large"; - statuses[414] = "Request-URI Too Large"; - statuses[416] = "Requested Range Not Satisfiable"; - statuses[417] = "Expectation Failed"; - statuses[500] = "Internal Server Error"; - statuses[501] = "Not Implemented"; - statuses[502] = "Bad Gateway"; - statuses[503] = "Service Unavailable"; - statuses[504] = "Gateway Time-out"; - } - - /** - * A mapping of path suffixes (e.g. file extensions) to their - * corresponding MIME types. - */ - protected static final Map contentTypes = - new ConcurrentHashMap(); - - static { - // add some default common content types - // see http://www.iana.org/assignments/media-types/ for full list - addContentType("application/font-woff", "woff"); - addContentType("application/font-woff2", "woff2"); - addContentType("application/java-archive", "jar"); - addContentType("application/javascript", "js"); - addContentType("application/json", "json"); - addContentType("application/octet-stream", "exe"); - addContentType("application/pdf", "pdf"); - addContentType("application/x-7z-compressed", "7z"); - addContentType("application/x-compressed", "tgz"); - addContentType("application/x-gzip", "gz"); - addContentType("application/x-tar", "tar"); - addContentType("application/xhtml+xml", "xhtml"); - addContentType("application/zip", "zip"); - addContentType("audio/mpeg", "mp3"); - addContentType("image/gif", "gif"); - addContentType("image/jpeg", "jpg", "jpeg"); - addContentType("image/png", "png"); - addContentType("image/svg+xml", "svg"); - addContentType("image/x-icon", "ico"); - addContentType("text/css", "css"); - addContentType("text/csv", "csv"); - addContentType("text/html; charset=utf-8", "htm", "html"); - addContentType("text/plain", "txt", "text", "log"); - addContentType("text/xml", "xml"); - } - - /** The MIME types that can be compressed (prefix/suffix wildcards allowed). */ - protected static String[] compressibleContentTypes = - { "text/*", "*/javascript", "*icon", "*+xml", "*/json" }; - - /** - * The {@code LimitedInputStream} provides access to a limited number - * of consecutive bytes from the underlying InputStream, starting at its - * current position. If this limit is reached, it behaves as though the end - * of stream has been reached (although the underlying stream remains open - * and may contain additional data). - */ - public static class LimitedInputStream extends FilterInputStream { - - protected long limit; // decremented when read, until it reaches zero - protected boolean prematureEndException; - - /** - * Constructs a LimitedInputStream with the given underlying - * input stream and limit. - * - * @param in the underlying input stream - * @param limit the maximum number of bytes that may be consumed from - * the underlying stream before this stream ends. If zero or - * negative, this stream will be at its end from initialization. - * @param prematureEndException specifies the stream's behavior when - * the underlying stream end is reached before the limit is - * reached: if true, an exception is thrown, otherwise this - * stream reaches its end as well (i.e. read() returns -1) - * @throws NullPointerException if the given stream is null - */ - public LimitedInputStream(InputStream in, long limit, boolean prematureEndException) { - super(in); - if (in == null) - throw new NullPointerException("input stream is null"); - this.limit = limit < 0 ? 0 : limit; - this.prematureEndException = prematureEndException; - } - - @Override - public int read() throws IOException { - int res = limit == 0 ? -1 : in.read(); - if (res < 0 && limit > 0 && prematureEndException) - throw new IOException("unexpected end of stream"); - limit = res < 0 ? 0 : limit - 1; - return res; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int res = limit == 0 ? -1 : in.read(b, off, len > limit ? (int)limit : len); - if (res < 0 && limit > 0 && prematureEndException) - throw new IOException("unexpected end of stream"); - limit = res < 0 ? 0 : limit - res; - return res; - } - - @Override - public long skip(long len) throws IOException { - long res = in.skip(len > limit ? limit : len); - limit -= res; - return res; - } - - @Override - public int available() throws IOException { - int res = in.available(); - return res > limit ? (int)limit : res; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() { - limit = 0; // end this stream, but don't close the underlying stream - } - } - - /** - * The {@code ChunkedInputStream} decodes an InputStream whose data has the - * "chunked" transfer encoding applied to it, providing the underlying data. - */ - public static class ChunkedInputStream extends LimitedInputStream { - - protected Headers headers; - protected boolean initialized; - - /** - * Constructs a ChunkedInputStream with the given underlying stream, and - * a headers container to which the stream's trailing headers will be - * added. - * - * @param in the underlying "chunked"-encoded input stream - * @param headers the headers container to which the stream's trailing - * headers will be added, or null if they are to be discarded - * @throws NullPointerException if the given stream is null - */ - public ChunkedInputStream(InputStream in, Headers headers) { - super(in, 0, true); - this.headers = headers; - } - - @Override - public int read() throws IOException { - return limit <= 0 && initChunk() < 0 ? -1 : super.read(); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return limit <= 0 && initChunk() < 0 ? -1 : super.read(b, off, len); - } - - /** - * Initializes the next chunk. If the previous chunk has not yet - * ended, or the end of stream has been reached, does nothing. - * - * @return the length of the chunk, or -1 if the end of stream - * has been reached - * @throws IOException if an IO error occurs or the stream is corrupt - */ - protected long initChunk() throws IOException { - if (limit == 0) { // finished previous chunk - // read chunk-terminating CRLF if it's not the first chunk - if (initialized && readLine(in).length() > 0) - throw new IOException("chunk data must end with CRLF"); - initialized = true; - limit = parseChunkSize(readLine(in)); // read next chunk size - if (limit == 0) { // last chunk has size 0 - limit = -1; // mark end of stream - // read trailing headers, if any - Headers trailingHeaders = readHeaders(in); - if (headers != null) - headers.addAll(trailingHeaders); - } - } - return limit; - } - - /** - * Parses a chunk-size line. - * - * @param line the chunk-size line to parse - * @return the chunk size - * @throws IllegalArgumentException if the chunk-size line is invalid - */ - protected static long parseChunkSize(String line) throws IllegalArgumentException { - int pos = line.indexOf(';'); - line = pos < 0 ? line : line.substring(0, pos); // ignore params, if any - try { - return parseULong(line, 16); // throws NFE - } catch (NumberFormatException nfe) { - throw new IllegalArgumentException( - "invalid chunk size line: \"" + line + "\""); - } - } - } - - /** - * The {@code ChunkedOutputStream} encodes an OutputStream with the - * "chunked" transfer encoding. It should be used only when the content - * length is not known in advance, and with the response Transfer-Encoding - * header set to "chunked". - *

- * Data is written to the stream by calling the {@link #write(byte[], int, int)} - * method, which writes a new chunk per invocation. To end the stream, - * the {@link #writeTrailingChunk} method must be called or the stream closed. - */ - public static class ChunkedOutputStream extends FilterOutputStream { - - protected int state; // the current stream state - - /** - * Constructs a ChunkedOutputStream with the given underlying stream. - * - * @param out the underlying output stream to which the chunked stream - * is written - * @throws NullPointerException if the given stream is null - */ - public ChunkedOutputStream(OutputStream out) { - super(out); - if (out == null) - throw new NullPointerException("output stream is null"); - } - - /** - * Initializes a new chunk with the given size. - * - * @param size the chunk size (must be positive) - * @throws IllegalArgumentException if size is negative - * @throws IOException if an IO error occurs, or the stream has - * already been ended - */ - protected void initChunk(long size) throws IOException { - if (size < 0) - throw new IllegalArgumentException("invalid size: " + size); - if (state > 0) - out.write(CRLF); // end previous chunk - else if (state == 0) - state = 1; // start first chunk - else - throw new IOException("chunked stream has already ended"); - out.write(getBytes(Long.toHexString(size))); - out.write(CRLF); - } - - /** - * Writes the trailing chunk which marks the end of the stream. - * - * @param headers the (optional) trailing headers to write, or null - * @throws IOException if an error occurs - */ - public void writeTrailingChunk(Headers headers) throws IOException { - initChunk(0); // zero-sized chunk marks the end of the stream - if (headers == null) - out.write(CRLF); // empty header block - else - headers.writeTo(out); - state = -1; - } - - /** - * Writes a chunk containing the given byte. This method initializes - * a new chunk of size 1, and then writes the byte as the chunk data. - * - * @param b the byte to write as a chunk - * @throws IOException if an error occurs - */ - @Override - public void write(int b) throws IOException { - write(new byte[] { (byte)b }, 0, 1); - } - - /** - * Writes a chunk containing the given bytes. This method initializes - * a new chunk of the given size, and then writes the chunk data. - * - * @param b an array containing the bytes to write - * @param off the offset within the array where the data starts - * @param len the length of the data in bytes - * @throws IOException if an error occurs - * @throws IndexOutOfBoundsException if the given offset or length - * are outside the bounds of the given array - */ - @Override - public void write(byte[] b, int off, int len) throws IOException { - if (len > 0) // zero-sized chunk is the trailing chunk - initChunk(len); - out.write(b, off, len); - } - - /** - * Writes the trailing chunk if necessary, and closes the underlying stream. - * - * @throws IOException if an error occurs - */ - @Override - public void close() throws IOException { - if (state > -1) - writeTrailingChunk(null); - super.close(); - } - } - - /** - * The {@code MultipartInputStream} decodes an InputStream whose data has - * a "multipart/*" content type (see RFC 2046), providing the underlying - * data of its various parts. - *

- * The {@code InputStream} methods (e.g. {@link #read}) relate only to - * the current part, and the {@link #nextPart} method advances to the - * beginning of the next part. - */ - public static class MultipartInputStream extends FilterInputStream { - - protected final byte[] boundary; // including leading CRLF-- - protected final byte[] buf = new byte[4096]; - protected int head, tail; // indices of current part's data in buf - protected int end; // last index of input data read into buf - protected int len; // length of found boundary - protected int state; // initial, started data, start boundary, EOS, last boundary, epilogue - - /** - * Constructs a MultipartInputStream with the given underlying stream. - * - * @param in the underlying multipart stream - * @param boundary the multipart boundary - * @throws NullPointerException if the given stream or boundary is null - * @throws IllegalArgumentException if the given boundary's size is not - * between 1 and 70 - */ - protected MultipartInputStream(InputStream in, byte[] boundary) { - super(in); - int len = boundary.length; - if (len == 0 || len > 70) - throw new IllegalArgumentException("invalid boundary length"); - this.boundary = new byte[len + 4]; // CRLF--boundary - System.arraycopy(CRLF, 0, this.boundary, 0, 2); - this.boundary[2] = this.boundary[3] = '-'; - System.arraycopy(boundary, 0, this.boundary, 4, len); - } - - @Override - public int read() throws IOException { - if (!fill()) - return -1; - return buf[head++] & 0xFF; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (!fill()) - return -1; - len = Math.min(tail - head, len); - System.arraycopy(buf, head, b, off, len); // throws IOOBE as necessary - head += len; - return len; - } - - @Override - public long skip(long len) throws IOException { - if (len <= 0 || !fill()) - return 0; - len = Math.min(tail - head, len); - head += len; - return len; - } - - @Override - public int available() throws IOException { - return tail - head; - } - - @Override - public boolean markSupported() { - return false; - } - - /** - * Advances the stream position to the beginning of the next part. - * Data read before calling this method for the first time is the preamble, - * and data read after this method returns false is the epilogue. - * - * @return true if successful, or false if there are no more parts - * @throws IOException if an error occurs - */ - public boolean nextPart() throws IOException { - while (skip(buf.length) != 0); // skip current part (until boundary) - head = tail += len; // the next part starts right after boundary - state |= 1; // started data (after first boundary) - if (state >= 8) { // found last boundary - state |= 0x10; // now beyond last boundary (epilogue) - return false; - } - findBoundary(); // update indices - return true; - } - - /** - * Fills the buffer with more data from the underlying stream. - * - * @return true if there is available data for the current part, - * or false if the current part's end has been reached - * @throws IOException if an error occurs or the input format is invalid - */ - protected boolean fill() throws IOException { - // check if we already have more available data - if (head != tail) // remember that if we continue, head == tail below - return true; - // if there's no more room, shift extra unread data to beginning of buffer - if (tail > buf.length - 256) { // max boundary + whitespace supported size - System.arraycopy(buf, tail, buf, 0, end -= tail); - head = tail = 0; - } - // read more data and look for boundary (or potential partial boundary) - int read; - do { - read = super.read(buf, end, buf.length - end); - if (read < 0) - state |= 4; // end of stream (EOS) - else - end += read; - findBoundary(); // updates tail and length to next potential boundary - // if we found a partial boundary with no data before it, we must - // continue reading to determine if there is more data or not - } while (read > 0 && tail == head && len == 0); - // update and validate state - if (tail != 0) // anything but a boundary right at the beginning - state |= 1; // started data (preamble or after boundary) - if (state < 8 && len > 0) - state |= 2; // found start boundary - if ((state & 6) == 4 // EOS but no start boundary found - || len == 0 && ((state & 0xFC) == 4 // EOS but no last and no more boundaries - || read == 0 && tail == head)) // boundary longer than buffer - throw new IOException("missing boundary"); - if (state >= 0x10) // in epilogue - tail = end; // ignore boundaries, return everything - return tail > head; // available data in current part - } - - /** - * Finds the first (potential) boundary within the buffer's remaining data. - * Updates tail, length and state fields accordingly. - * - * @throws IOException if an error occurs or the input format is invalid - */ - protected void findBoundary() throws IOException { - // see RFC2046#5.1.1 for boundary syntax - len = 0; - int off = tail - ((state & 1) != 0 || buf[0] != '-' ? 0 : 2); // skip initial CRLF? - for (int end = this.end; tail < end; tail++, off = tail) { - int j = tail; // end of potential boundary - // try to match boundary value (leading CRLF is optional at first boundary) - while (j < end && j - off < boundary.length && buf[j] == boundary[j - off]) - j++; - // return potential partial boundary which is cut off at end of current data - if (j + 1 >= end) // at least two more chars needed for full boundary (CRLF or --) - return; - // if we found the boundary value, expand selection to include full line - if (j - off == boundary.length) { - // check if last boundary of entire multipart - if (buf[j] == '-' && buf[j + 1] == '-') { - j += 2; - state |= 8; // found last boundary that ends multipart - } - // allow linear whitespace after boundary - while (j < end && (buf[j] == ' ' || buf[j] == '\t')) - j++; - // check for CRLF (required, except in last boundary with no epilogue) - if (j + 1 < end && buf[j] == '\r' && buf[j + 1] == '\n') // found CRLF - len = j - tail + 2; // including optional whitespace and CRLF - else if (j + 1 < end || (state & 4) != 0 && j + 1 == end) // should have found or never will - throw new IOException("boundary must end with CRLF"); - else if ((state & 4) != 0) // last boundary with no CRLF at end of data is valid - len = j - tail; - return; - } - } - } - } - - /** - * The {@code MultipartIterator} iterates over the parts of a multipart/form-data request. - *

- * For example, to support file upload from a web browser: - *

    - *
  1. Create an HTML form which includes an input field of type "file", attributes - * method="post" and enctype="multipart/form-data", and an action URL of your choice, - * for example action="/upload". This form can be served normally like any other - * resource, e.g. from an HTML file on disk. - *
  2. Add a context handler for the action path ("/upload" in this example), using either - * the explicit {@link VirtualHost#addContext} method or the {@link Context} annotation. - *
  3. In the context handler implementation, construct a {@code MultipartIterator} from - * the client {@code Request}. - *
  4. Iterate over the form {@link Part}s, processing each named field as appropriate - - * for the file input field, read the uploaded file using the body input stream. - *
- */ - public static class MultipartIterator implements Iterator { - - /** - * The {@code Part} class encapsulates a single part of the multipart. - */ - public static class Part { - - public String name; - public String filename; - public Headers headers; - public InputStream body; - - /** - * Returns the part's name (form field name). - * - * @return the part's name - */ - public String getName() { return name; } - - /** - * Returns the part's filename (original filename entered in file form field). - * - * @return the part's filename, or null if there is none - */ - public String getFilename() { return filename; } - - /** - * Returns the part's headers. - * - * @return the part's headers - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the part's body (form field value). - * - * @return the part's body - */ - public InputStream getBody() { return body; } - - /*** - * Returns the part's body as a string. If the part - * headers do not specify a charset, UTF-8 is used. - * - * @return the part's body as a string - * @throws IOException if an IO error occurs - */ - public String getString() throws IOException { - String charset = headers.getParams("Content-Type").get("charset"); - return readToken(body, -1, charset == null ? "UTF-8" : charset, 8192); - } - } - - protected final MultipartInputStream in; - protected boolean next; - - /** - * Creates a new MultipartIterator from the given request. - * - * @param req the multipart/form-data request - * @throws IOException if an IO error occurs - * @throws IllegalArgumentException if the given request's content type - * is not multipart/form-data, or is missing the boundary - */ - public MultipartIterator(Request req) throws IOException { - Map ct = req.getHeaders().getParams("Content-Type"); - if (!ct.containsKey("multipart/form-data")) - throw new IllegalArgumentException("Content-Type is not multipart/form-data"); - String boundary = ct.get("boundary"); // should be US-ASCII - if (boundary == null) - throw new IllegalArgumentException("Content-Type is missing boundary"); - in = new MultipartInputStream(req.getBody(), getBytes(boundary)); - } - - public boolean hasNext() { - try { - return next || (next = in.nextPart()); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - - public Part next() { - if (!hasNext()) - throw new NoSuchElementException(); - next = false; - Part p = new Part(); - try { - p.headers = readHeaders(in); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - Map cd = p.headers.getParams("Content-Disposition"); - p.name = cd.get("name"); - p.filename = cd.get("filename"); - p.body = in; - return p; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } - - /** - * The {@code VirtualHost} class represents a virtual host in the server. - */ - public static class VirtualHost { - - public static final String DEFAULT_HOST_NAME = "~DEFAULT~"; - - /** - * The {@code ContextInfo} class holds a single context's information. - */ - public class ContextInfo { - - protected final String path; - protected final Map handlers = - new ConcurrentHashMap(2); - - /** - * Constructs a ContextInfo with the given context path. - * - * @param path the context path (without trailing slash) - */ - public ContextInfo(String path) { - this.path = path; - } - - /** - * Returns the context path. - * - * @return the context path, or null if there is none - */ - public String getPath() { - return path; - } - - /** - * Returns the map of supported HTTP methods and their corresponding handlers. - * - * @return the map of supported HTTP methods and their corresponding handlers - */ - public Map getHandlers() { - return handlers; - } - - /** - * Adds (or replaces) a context handler for the given HTTP methods. - * - * @param handler the context handler - * @param methods the HTTP methods supported by the handler (default is "GET") - */ - public void addHandler(ContextHandler handler, String... methods) { - if (methods.length == 0) - methods = new String[] { "GET" }; - for (String method : methods) { - handlers.put(method, handler); - VirtualHost.this.methods.add(method); // it's now supported by server - } - } - } - - protected final String name; - protected final Set aliases = new CopyOnWriteArraySet(); - protected volatile String directoryIndex = "index.html"; - protected volatile boolean allowGeneratedIndex; - protected final Set methods = new CopyOnWriteArraySet(); - protected final ContextInfo emptyContext = new ContextInfo(null); - protected final ConcurrentMap contexts = - new ConcurrentHashMap(); - - /** - * Constructs a VirtualHost with the given name. - * - * @param name the host's name, or null if it is the default host - */ - public VirtualHost(String name) { - this.name = name; - contexts.put("*", new ContextInfo(null)); // for "OPTIONS *" - } - - /** - * Returns this host's name. - * - * @return this host's name, or null if it is the default host - */ - public String getName() { - return name; - } - - /** - * Adds an alias for this host. - * - * @param alias the alias - */ - public void addAlias(String alias) { - aliases.add(alias); - } - - /** - * Returns this host's aliases. - * - * @return the (unmodifiable) set of aliases (which may be empty) - */ - public Set getAliases() { - return Collections.unmodifiableSet(aliases); - } - - /** - * Sets the directory index file. For every request whose URI ends with - * a '/' (i.e. a directory), the index file is appended to the path, - * and the resulting resource is served if it exists. If it does not - * exist, an auto-generated index for the requested directory may be - * served, depending on whether {@link #setAllowGeneratedIndex - * a generated index is allowed}, otherwise an error is returned. - * The default directory index file is "index.html". - * - * @param directoryIndex the directory index file, or null if no - * index file should be used - */ - public void setDirectoryIndex(String directoryIndex) { - this.directoryIndex = directoryIndex; - } - - /** - * Gets this host's directory index file. - * - * @return the directory index file, or null - */ - public String getDirectoryIndex() { - return directoryIndex; - } - - /** - * Sets whether auto-generated indices are allowed. If false, and a - * directory resource is requested, an error will be returned instead. - * - * @param allowed specifies whether generated indices are allowed - */ - public void setAllowGeneratedIndex(boolean allowed) { - this.allowGeneratedIndex = allowed; - } - - /** - * Returns whether auto-generated indices are allowed. - * - * @return whether auto-generated indices are allowed - */ - public boolean isAllowGeneratedIndex() { - return allowGeneratedIndex; - } - - /** - * Returns all HTTP methods explicitly supported by at least one context - * (this may or may not include the methods with required or built-in support). - * - * @return all HTTP methods explicitly supported by at least one context - */ - public Set getMethods() { - return methods; - } - - /** - * Returns the context handler for the given path. - * - * If a context is not found for the given path, the search is repeated for - * its parent path, and so on until a base context is found. If neither the - * given path nor any of its parents has a context, an empty context is returned. - * - * @param path the context's path - * @return the context info for the given path, or an empty context if none exists - */ - public ContextInfo getContext(String path) { - // all context paths are without trailing slash - for (path = trimRight(path, '/'); path != null; path = getParentPath(path)) { - ContextInfo info = contexts.get(path); - if (info != null) - return info; - } - return emptyContext; - } - - /** - * Adds a context and its corresponding context handler to this server. - * Paths are normalized by removing trailing slashes (except the root). - * - * @param path the context's path (must start with '/') - * @param handler the context handler for the given path - * @param methods the HTTP methods supported by the context handler (default is "GET") - * @throws IllegalArgumentException if path is malformed - */ - public void addContext(String path, ContextHandler handler, String... methods) { - if (path == null || !path.startsWith("/") && !path.equals("*")) - throw new IllegalArgumentException("invalid path: " + path); - path = trimRight(path, '/'); // remove trailing slash - ContextInfo info = new ContextInfo(path); - ContextInfo existing = contexts.putIfAbsent(path, info); - info = existing != null ? existing : info; - info.addHandler(handler, methods); - } - - /** - * Adds contexts for all methods of the given object that - * are annotated with the {@link Context} annotation. - * - * @param o the object whose annotated methods are added - * @throws IllegalArgumentException if a Context-annotated - * method has an {@link Context invalid signature} - */ - public void addContexts(Object o) throws IllegalArgumentException { - for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { - // add to contexts those with @Context annotation - for (Method m : c.getDeclaredMethods()) { - Context context = m.getAnnotation(Context.class); - if (context != null) { - m.setAccessible(true); // allow access to private method - ContextHandler handler = new MethodContextHandler(m, o); - addContext(context.value(), handler, context.methods()); - } - } - } - } - } - - /** - * The {@code Context} annotation decorates methods which are mapped - * to a context (path) within the server, and provide its contents. - *

- * The annotated methods must have the same signature and contract - * as {@link ContextHandler#serve}, but can have arbitrary names. - * - * @see VirtualHost#addContexts(Object) - */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - public @interface Context { - - /** - * The context (path) that this field maps to (must begin with '/'). - * - * @return the context (path) that this field maps to - */ - String value(); - - /** - * The HTTP methods supported by this context handler (default is "GET"). - * - * @return the HTTP methods supported by this context handler - */ - String[] methods() default "GET"; - } - - /** - * A {@code ContextHandler} serves the content of resources within a context. - * - * @see VirtualHost#addContext - */ - public interface ContextHandler { - - /** - * Serves the given request using the given response. - * - * @param req the request to be served - * @param resp the response to be filled - * @return an HTTP status code, which will be used in returning - * a default response appropriate for this status. If this - * method invocation already sent anything in the response - * (headers or content), it must return 0, and no further - * processing will be done - * @throws IOException if an IO error occurs - */ - int serve(Request req, Response resp) throws IOException; - } - - /** - * The {@code FileContextHandler} services a context by mapping it - * to a file or folder (recursively) on disk. - */ - public static class FileContextHandler implements ContextHandler { - - protected final File base; - - public FileContextHandler(File dir) throws IOException { - this.base = dir.getCanonicalFile(); - } - - public int serve(Request req, Response resp) throws IOException { - return serveFile(base, req.getContext().getPath(), req, resp); - } - } - - /** - * The {@code MethodContextHandler} services a context - * by invoking a handler method on a specified object. - *

- * The method must have the same signature and contract as - * {@link ContextHandler#serve}, but can have an arbitrary name. - * - * @see VirtualHost#addContexts(Object) - */ - public static class MethodContextHandler implements ContextHandler { - - protected final Method m; - protected final Object obj; - - public MethodContextHandler(Method m, Object obj) throws IllegalArgumentException { - this.m = m; - this.obj = obj; - Class[] params = m.getParameterTypes(); - if (params.length != 2 - || !Request.class.isAssignableFrom(params[0]) - || !Response.class.isAssignableFrom(params[1]) - || !int.class.isAssignableFrom(m.getReturnType())) - throw new IllegalArgumentException("invalid method signature: " + m); - } - - public int serve(Request req, Response resp) throws IOException { - try { - return (Integer)m.invoke(obj, req, resp); - } catch (InvocationTargetException ite) { - throw new IOException("error: " + ite.getCause().getMessage()); - } catch (Exception e) { - throw new IOException("error: " + e); - } - } - } - - /** - * The {@code Header} class encapsulates a single HTTP header. - */ - public static class Header { - - protected final String name; - protected final String value; - - /** - * Constructs a header with the given name and value. - * Leading and trailing whitespace are trimmed. - * - * @param name the header name - * @param value the header value - * @throws NullPointerException if name or value is null - * @throws IllegalArgumentException if name is empty - */ - public Header(String name, String value) { - this.name = name.trim(); - this.value = value.trim(); - // RFC2616#14.23 - header can have an empty value (e.g. Host) - if (this.name.length() == 0) // but name cannot be empty - throw new IllegalArgumentException("name cannot be empty"); - } - - /** - * Returns this header's name. - * - * @return this header's name - */ - public String getName() { return name; } - - /** - * Returns this header's value. - * - * @return this header's value - */ - public String getValue() { return value; } - } - - /** - * The {@code Headers} class encapsulates a collection of HTTP headers. - * - * Header names are treated case-insensitively, although this class retains - * their original case. Header insertion order is maintained as well. - */ - public static class Headers implements Iterable

{ - - // due to the requirements of case-insensitive name comparisons, - // retaining the original case, and retaining header insertion order, - // and due to the fact that the number of headers is generally - // quite small (usually under 12 headers), we use a simple array with - // linear access times, which proves to be more efficient and - // straightforward than the alternatives - protected Header[] headers = new Header[12]; - protected int count; - - /** - * Returns the number of added headers. - * - * @return the number of added headers - */ - public int size() { - return count; - } - - /** - * Returns the value of the first header with the given name. - * - * @param name the header name (case insensitive) - * @return the header value, or null if none exists - */ - public String get(String name) { - for (int i = 0; i < count; i++) - if (headers[i].getName().equalsIgnoreCase(name)) - return headers[i].getValue(); - return null; - } - - /** - * Returns the Date value of the header with the given name. - * - * @param name the header name (case insensitive) - * @return the header value as a Date, or null if none exists - * or if the value is not in any supported date format - */ - public Date getDate(String name) { - try { - String header = get(name); - return header == null ? null : parseDate(header); - } catch (IllegalArgumentException iae) { - return null; - } - } - - /** - * Returns whether there exists a header with the given name. - * - * @param name the header name (case insensitive) - * @return whether there exists a header with the given name - */ - public boolean contains(String name) { - return get(name) != null; - } - - /** - * Adds a header with the given name and value to the end of this - * collection of headers. Leading and trailing whitespace are trimmed. - * - * @param name the header name (case insensitive) - * @param value the header value - */ - public void add(String name, String value) { - Header header = new Header(name, value); // also validates - // expand array if necessary - if (count == headers.length) { - Header[] expanded = new Header[2 * count]; - System.arraycopy(headers, 0, expanded, 0, count); - headers = expanded; - } - headers[count++] = header; // inlining header would cause a bug! - } - - /** - * Adds all given headers to the end of this collection of headers, - * in their original order. - * - * @param headers the headers to add - */ - public void addAll(Headers headers) { - for (Header header : headers) - add(header.getName(), header.getValue()); - } - - /** - * Adds a header with the given name and value, replacing the first - * existing header with the same name. If there is no existing header - * with the same name, it is added as in {@link #add}. - * - * @param name the header name (case insensitive) - * @param value the header value - * @return the replaced header, or null if none existed - */ - public Header replace(String name, String value) { - for (int i = 0; i < count; i++) { - if (headers[i].getName().equalsIgnoreCase(name)) { - Header prev = headers[i]; - headers[i] = new Header(name, value); - return prev; - } - } - add(name, value); - return null; - } - - /** - * Removes all headers with the given name (if any exist). - * - * @param name the header name (case insensitive) - */ - public void remove(String name) { - int j = 0; - for (int i = 0; i < count; i++) - if (!headers[i].getName().equalsIgnoreCase(name)) - headers[j++] = headers[i]; - while (count > j) - headers[--count] = null; - } - - /** - * Writes the headers to the given stream (including trailing CRLF). - * - * @param out the stream to write the headers to - * @throws IOException if an error occurs - */ - public void writeTo(OutputStream out) throws IOException { - for (int i = 0; i < count; i++) { - out.write(getBytes(headers[i].getName(), ": ", headers[i].getValue())); - out.write(CRLF); - } - out.write(CRLF); // ends header block - } - - /** - * Returns a header's parameters. Parameter order is maintained, - * and the first key (in iteration order) is the header's value - * without the parameters. - * - * @param name the header name (case insensitive) - * @return the header's parameter names and values - */ - public Map getParams(String name) { - Map params = new LinkedHashMap(); - for (String param : split(get(name), ";", -1)) { - String[] pair = split(param, "=", 2); - String val = pair.length == 1 ? "" : trimLeft(trimRight(pair[1], '"'), '"'); - params.put(pair[0], val); - } - return params; - } - - /** - * Returns an iterator over the headers, in their insertion order. - * If the headers collection is modified during iteration, the - * iteration result is undefined. The remove operation is unsupported. - * - * @return an Iterator over the headers - */ - public Iterator
iterator() { - // we use the built-in wrapper instead of a trivial custom implementation - // since even a tiny anonymous class here compiles to a 1.5K class file - return Arrays.asList(headers).subList(0, count).iterator(); - } - } - - /** - * The {@code Request} class encapsulates a single HTTP request. - */ - public class Request { - - protected String method; - protected URI uri; - protected URL baseURL; // cached value - protected String version; - protected Headers headers; - protected InputStream body; - protected Map params; // cached value - protected VirtualHost host; // cached value - protected VirtualHost.ContextInfo context; // cached value - - /** - * Constructs a Request from the data in the given input stream. - * - * @param in the input stream from which the request is read - * @throws IOException if an error occurs - */ - public Request(InputStream in) throws IOException { - readRequestLine(in); - headers = readHeaders(in); - // RFC2616#3.6 - if "chunked" is used, it must be the last one - // RFC2616#4.4 - if non-identity Transfer-Encoding is present, - // it must either include "chunked" or close the connection after - // the body, and in any case ignore Content-Length. - // if there is no such Transfer-Encoding, use Content-Length - // if neither header exists, there is no body - String header = headers.get("Transfer-Encoding"); - if (header != null && !header.toLowerCase(Locale.US).equals("identity")) { - if (Arrays.asList(splitElements(header, true)).contains("chunked")) - body = new ChunkedInputStream(in, headers); - else - body = in; // body ends when connection closes - } else { - header = headers.get("Content-Length"); - long len = header == null ? 0 : parseULong(header, 10); - body = new LimitedInputStream(in, len, false); - } - } - - /** - * Returns the request method. - * - * @return the request method - */ - public String getMethod() { return method; } - - /** - * Returns the request URI. - * - * @return the request URI - */ - public URI getURI() { return uri; } - - /** - * Returns the request version string. - * - * @return the request version string - */ - public String getVersion() { return version; } - - /** - * Returns the request headers. - * - * @return the request headers - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the input stream containing the request body. - * - * @return the input stream containing the request body - */ - public InputStream getBody() { return body; } - - /** - * Returns the body in a String format - */ - public String getJson(){ - Scanner s = new Scanner(body).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } - - /** - * Returns the path component of the request URI, after - * URL decoding has been applied (using the UTF-8 charset). - * - * @return the decoded path component of the request URI - */ - public String getPath() { - return uri.getPath(); - } - - /** - * Sets the path component of the request URI. This can be useful - * in URL rewriting, etc. - * - * @param path the path to set - * @throws IllegalArgumentException if the given path is malformed - */ - public void setPath(String path) { - try { - uri = new URI(uri.getScheme(), uri.getHost(), - trimDuplicates(path, '/'), uri.getFragment()); - context = null; // clear cached context so it will be recalculated - } catch (URISyntaxException use) { - throw new IllegalArgumentException("error setting path", use); - } - } - - /** - * Returns the base URL (scheme, host and port) of the request resource. - * The host name is taken from the request URI or the Host header or a - * default host (see RFC2616#5.2). - * - * @return the base URL of the requested resource, or null if it - * is malformed - */ - public URL getBaseURL() { - if (baseURL != null) - return baseURL; - // normalize host header - String host = uri.getHost(); - if (host == null) { - host = headers.get("Host"); - if (host == null) // missing in HTTP/1.0 - host = detectLocalHostName(); - } - int pos = host.indexOf(':'); - host = pos < 0 ? host : host.substring(0, pos); - try { - return baseURL = new URL(secure ? "https" : "http", host, port, ""); - } catch (MalformedURLException mue) { - return null; - } - } - - /** - * Returns the request parameters, which are parsed both from the query - * part of the request URI, and from the request body if its content - * type is "application/x-www-form-urlencoded" (i.e. a submitted form). - * UTF-8 encoding is assumed in both cases. - *

- * The parameters are returned as a list of string arrays, each containing - * the parameter name as the first element and its corresponding value - * as the second element (or an empty string if there is no value). - *

- * The list retains the original order of the parameters. - * - * @return the request parameters name-value pairs, - * or an empty list if there are none - * @throws IOException if an error occurs - * @see HTTPServer#parseParamsList(String) - */ - public List getParamsList() throws IOException { - List queryParams = parseParamsList(uri.getRawQuery()); - List bodyParams = Collections.emptyList(); - String ct = headers.get("Content-Type"); - if (ct != null && ct.toLowerCase(Locale.US).startsWith("application/x-www-form-urlencoded")) - bodyParams = parseParamsList(readToken(body, -1, "UTF-8", 2097152)); // 2MB limit - if (bodyParams.isEmpty()) - return queryParams; - if (queryParams.isEmpty()) - return bodyParams; - queryParams.addAll(bodyParams); - return queryParams; - } - - /** - * Returns the request parameters, which are parsed both from the query - * part of the request URI, and from the request body if its content - * type is "application/x-www-form-urlencoded" (i.e. a submitted form). - * UTF-8 encoding is assumed in both cases. - *

- * For multivalued parameters (i.e. multiple parameters with the same - * name), only the first one is considered. For access to all values, - * use {@link #getParamsList()} instead. - *

- * The map iteration retains the original order of the parameters. - * - * @return the request parameters name-value pairs, - * or an empty map if there are none - * @throws IOException if an error occurs - * @see #getParamsList() - */ - public Map getParams() throws IOException { - if (params == null) - params = toMap(getParamsList()); - return params; - } - - /** - * Returns the absolute (zero-based) content range value read - * from the Range header. If multiple ranges are requested, a single - * range containing all of them is returned. - * - * @param length the full length of the requested resource - * @return the requested range, or null if the Range header - * is missing or invalid - */ - public long[] getRange(long length) { - String header = headers.get("Range"); - return header == null || !header.startsWith("bytes=") - ? null : parseRange(header.substring(6), length); - } - - /** - * Reads the request line, parsing the method, URI and version string. - * - * @param in the input stream from which the request line is read - * @throws IOException if an error occurs or the request line is invalid - */ - protected void readRequestLine(InputStream in) throws IOException { - // RFC2616#4.1: should accept empty lines before request line - // RFC2616#19.3: tolerate additional whitespace between tokens - String line; - try { - do { line = readLine(in); } while (line.length() == 0); - } catch (IOException ioe) { // if EOF, timeout etc. - throw new IOException("missing request line"); // signal that the request did not begin - } - String[] tokens = split(line, " ", -1); - if (tokens.length != 3) - throw new IOException("invalid request line: \"" + line + "\""); - try { - method = tokens[0]; - // must remove '//' prefix which constructor parses as host name - uri = new URI(trimDuplicates(tokens[1], '/')); - version = tokens[2]; // RFC2616#2.1: allow implied LWS; RFC7230#3.1.1: disallow it - } catch (URISyntaxException use) { - throw new IOException("invalid URI: " + use.getMessage()); - } - } - - /** - * Returns the virtual host corresponding to the requested host name, - * or the default host if none exists. - * - * @return the virtual host corresponding to the requested host name, - * or the default virtual host - */ - public VirtualHost getVirtualHost() { - return host != null ? host - : (host = HTTPServer.this.getVirtualHost(getBaseURL().getHost())) != null ? host - : (host = HTTPServer.this.getVirtualHost(null)); - } - - /** - * Returns the info of the context handling this request. - * - * @return the info of the context handling this request, or an empty context - */ - public VirtualHost.ContextInfo getContext() { - return context != null ? context : (context = getVirtualHost().getContext(getPath())); - } - } - - /** - * The {@code Response} class encapsulates a single HTTP response. - */ - public class Response implements Closeable { - - protected OutputStream out; // the underlying output stream - protected OutputStream[] encoders = new OutputStream[4]; // chained encoder streams - protected Headers headers; - protected boolean discardBody; - protected int state; // nothing sent, headers sent, or closed - protected Request req; // request used in determining client capabilities - - /** - * Constructs a Response whose output is written to the given stream. - * - * @param out the stream to which the response is written - */ - public Response(OutputStream out) { - this.out = out; - this.headers = new Headers(); - } - - /** - * Sets whether this response's body is discarded or sent. - * - * @param discardBody specifies whether the body is discarded or not - */ - public void setDiscardBody(boolean discardBody) { - this.discardBody = discardBody; - } - - /** - * Sets the request which is used in determining the capabilities - * supported by the client (e.g. compression, encoding, etc.) - * - * @param req the request - */ - public void setClientCapabilities(Request req) { this.req = req; } - - /** - * Returns the request headers collection. - * - * @return the request headers collection - */ - public Headers getHeaders() { return headers; } - - /** - * Returns the underlying output stream to which the response is written. - * Except for special cases, you should use {@link #getBody()} instead. - * - * @return the underlying output stream to which the response is written - */ - public OutputStream getOutputStream() { return out; } - - /** - * Returns whether the response headers were already sent. - * - * @return whether the response headers were already sent - */ - public boolean headersSent() { return state == 1; } - - /** - * Returns an output stream into which the response body can be written. - * The stream applies encodings (e.g. compression) according to the sent headers. - * This method must be called after response headers have been sent - * that indicate there is a body. Normally, the content should be - * prepared (not sent) even before the headers are sent, so that any - * errors during processing can be caught and a proper error response returned - - * after the headers are sent, it's too late to change the status into an error. - * - * @return an output stream into which the response body can be written, - * or null if the body should not be written (e.g. it is discarded) - * @throws IOException if an error occurs - */ - public OutputStream getBody() throws IOException { - if (encoders[0] != null || discardBody) - return encoders[0]; // return the existing stream (or null) - // set up chain of encoding streams according to headers - List te = Arrays.asList(splitElements(headers.get("Transfer-Encoding"), true)); - List ce = Arrays.asList(splitElements(headers.get("Content-Encoding"), true)); - int i = encoders.length - 1; - encoders[i] = new FilterOutputStream(out) { - @Override - public void close() {} // keep underlying connection stream open for now - @Override // override the very inefficient default implementation - public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } - }; - if (te.contains("chunked")) - encoders[--i] = new ChunkedOutputStream(encoders[i + 1]); - if (ce.contains("gzip") || te.contains("gzip")) - encoders[--i] = new GZIPOutputStream(encoders[i + 1], 4096); - else if (ce.contains("deflate") || te.contains("deflate")) - encoders[--i] = new DeflaterOutputStream(encoders[i + 1]); - encoders[0] = encoders[i]; - encoders[i] = null; // prevent duplicate reference - return encoders[0]; // returned stream is always first - } - - /** - * Closes this response and flushes all output. - * - * @throws IOException if an error occurs - */ - public void close() throws IOException { - state = -1; // closed - if (encoders[0] != null) - encoders[0].close(); // close all chained streams (except the underlying one) - out.flush(); // always flush underlying stream (even if getBody was never called) - } - - /** - * Sends the response headers with the given response status. - * A Date header is added if it does not already exist. - * If the response has a body, the Content-Length/Transfer-Encoding - * and Content-Type headers must be set before sending the headers. - * - * @param status the response status - * @throws IOException if an error occurs or headers were already sent - * @see #sendHeaders(int, long, long, String, String, long[]) - */ - public void sendHeaders(int status) throws IOException { - if (headersSent()) - throw new IOException("headers were already sent"); - if (!headers.contains("Date")) - headers.add("Date", formatDate(System.currentTimeMillis())); - headers.add("Server", "JLHTTP/2.4"); - out.write(getBytes("HTTP/1.1 ", Integer.toString(status), " ", statuses[status])); - out.write(CRLF); - headers.writeTo(out); - state = 1; // headers sent - } - - /** - * Sends the response headers, including the given response status - * and description, and all response headers. If they do not already - * exist, the following headers are added as necessary: - * Content-Range, Content-Type, Transfer-Encoding, Content-Encoding, - * Content-Length, Last-Modified, ETag, Connection and Date. Ranges are - * properly calculated as well, with a 200 status changed to a 206 status. - * - * @param status the response status - * @param length the response body length, or zero if there is no body, - * or negative if there is a body but its length is not yet known - * @param lastModified the last modified date of the response resource, - * or non-positive if unknown. A time in the future will be - * replaced with the current system time. - * @param etag the ETag of the response resource, or null if unknown - * (see RFC2616#3.11) - * @param contentType the content type of the response resource, or null - * if unknown (in which case "application/octet-stream" will be sent) - * @param range the content range that will be sent, or null if the - * entire resource will be sent - * @throws IOException if an error occurs - */ - public void sendHeaders(int status, long length, long lastModified, - String etag, String contentType, long[] range) throws IOException { - if (range != null) { - headers.add("Content-Range", "bytes " + range[0] + "-" + - range[1] + "/" + (length >= 0 ? length : "*")); - length = range[1] - range[0] + 1; - if (status == 200) - status = 206; - } - String ct = headers.get("Content-Type"); - if (ct == null) { - ct = contentType != null ? contentType : "application/octet-stream"; - headers.add("Content-Type", ct); - } - if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding")) { - // RFC2616#3.6: transfer encodings are case-insensitive and must not be sent to an HTTP/1.0 client - boolean modern = req != null && req.getVersion().endsWith("1.1"); - String accepted = req == null ? null : req.getHeaders().get("Accept-Encoding"); - List encodings = Arrays.asList(splitElements(accepted, true)); - String compression = encodings.contains("gzip") ? "gzip" : - encodings.contains("deflate") ? "deflate" : null; - if (compression != null && (length < 0 || length > 300) && isCompressible(ct) && modern) { - headers.add("Transfer-Encoding", "chunked"); // compressed data is always unknown length - headers.add("Content-Encoding", compression); - } else if (length < 0 && modern) { - headers.add("Transfer-Encoding", "chunked"); // unknown length - } else if (length >= 0) { - headers.add("Content-Length", Long.toString(length)); // known length - } - } - if (!headers.contains("Vary")) // RFC7231#7.1.4: Vary field should include headers - headers.add("Vary", "Accept-Encoding"); // that are used in selecting representation - if (lastModified > 0 && !headers.contains("Last-Modified")) // RFC2616#14.29 - headers.add("Last-Modified", formatDate(Math.min(lastModified, System.currentTimeMillis()))); - if (etag != null && !headers.contains("ETag")) - headers.add("ETag", etag); - if (req != null && "close".equalsIgnoreCase(req.getHeaders().get("Connection")) - && !headers.contains("Connection")) - headers.add("Connection", "close"); // #RFC7230#6.6: should reply to close with close - sendHeaders(status); - } - - /** - * Sends the full response with the given status, and the given string - * as the body. The text is sent in the UTF-8 charset. If a - * Content-Type header was not explicitly set, it will be set to - * text/html, and so the text must contain valid (and properly - * {@link HTTPServer#escapeHTML escaped}) HTML. - * - * @param status the response status - * @param text the text body (sent as text/html) - * @throws IOException if an error occurs - */ - public void send(int status, String text) throws IOException { - byte[] content = text.getBytes("UTF-8"); - sendHeaders(status, content.length, -1, - "W/\"" + Integer.toHexString(text.hashCode()) + "\"", - "text/html; charset=utf-8", null); - OutputStream out = getBody(); - if (out != null) - out.write(content); - } - - public void send(int status, byte[] text) throws IOException { - //byte[] content = text.getBytes("UTF-8"); - byte[] content = text; - sendHeaders(status, content.length, -1, - "W/\"" + Integer.toHexString(text.hashCode()) + "\"", - "text/html; charset=utf-8", null); - OutputStream out = getBody(); - if (out != null) - out.write(content); - } - - /** - * Sends an error response with the given status and detailed message. - * An HTML body is created containing the status and its description, - * as well as the message, which is escaped using the - * {@link HTTPServer#escapeHTML escape} method. - * - * @param status the response status - * @param text the text body (sent as text/html) - * @throws IOException if an error occurs - */ - public void sendError(int status, String text) throws IOException { - send(status, String.format( - "%n%n%d %s%n" + - "

%d %s

%n

%s

%n", - status, statuses[status], status, statuses[status], escapeHTML(text))); - } - - /** - * Sends an error response with the given status and default body. - * - * @param status the response status - * @throws IOException if an error occurs - */ - public void sendError(int status) throws IOException { - String text = status < 400 ? ":)" : "sorry it didn't work out :("; - sendError(status, text); - } - - /** - * Sends the response body. This method must be called only after the - * response headers have been sent (and indicate that there is a body). - * - * @param body a stream containing the response body - * @param length the full length of the response body - * @param range the sub-range within the response body that should be - * sent, or null if the entire body should be sent - * @throws IOException if an error occurs - */ - public void sendBody(InputStream body, long length, long[] range) throws IOException { - OutputStream out = getBody(); - if (out != null) { - if (range != null) { - long offset = range[0]; - length = range[1] - range[0] + 1; - while (offset > 0) { - long skip = body.skip(offset); - if (skip == 0) - throw new IOException("can't skip to " + range[0]); - offset -= skip; - } - } - transfer(body, out, length); - } - } - - /** - * Sends a 301 or 302 response, redirecting the client to the given URL. - * - * @param url the absolute URL to which the client is redirected - * @param permanent specifies whether a permanent (301) or - * temporary (302) redirect status is sent - * @throws IOException if an IO error occurs or url is malformed - */ - public void redirect(String url, boolean permanent) throws IOException { - try { - url = new URI(url).toASCIIString(); - } catch (URISyntaxException e) { - throw new IOException("malformed URL: " + url); - } - headers.add("Location", url); - // some user-agents expect a body, so we send it - if (permanent) - sendError(301, "Permanently moved to " + url); - else - sendError(302, "Temporarily moved to " + url); - } - } - - /** - * The {@code SocketHandlerThread} handles accepted sockets. - */ - protected class SocketHandlerThread extends Thread { - @Override - public void run() { - setName(getClass().getSimpleName() + "-" + port); - try { - ServerSocket serv = HTTPServer.this.serv; // keep local to avoid NPE when stopped - while (serv != null && !serv.isClosed()) { - final Socket sock = serv.accept(); - executor.execute(new Runnable() { - public void run() { - try { - try { - sock.setSoTimeout(socketTimeout); - sock.setTcpNoDelay(true); // we buffer anyway, so improve latency - handleConnection(sock.getInputStream(), sock.getOutputStream()); - } finally { - try { - // RFC7230#6.6 - close socket gracefully - // (except SSL socket which doesn't support half-closing) - if (!(sock instanceof SSLSocket)) { - sock.shutdownOutput(); // half-close socket (only output) - transfer(sock.getInputStream(), null, -1); // consume input - } - } finally { - sock.close(); // and finally close socket fully - } - } - } catch (IOException ignore) {} - } - }); - } - } catch (IOException ignore) {} - } - } - - protected volatile int port; - protected volatile int socketTimeout = 10000; - protected volatile ServerSocketFactory serverSocketFactory; - protected volatile boolean secure; - protected volatile Executor executor; - protected volatile ServerSocket serv; - protected final Map hosts = new ConcurrentHashMap(); - - /** - * Constructs an HTTPServer which can accept connections on the given port. - * Note: the {@link #start()} method must be called to start accepting - * connections. - * - * @param port the port on which this server will accept connections - */ - public HTTPServer(int port) { - setPort(port); - addVirtualHost(new VirtualHost(null)); // add default virtual host - } - - /** - * Constructs an HTTPServer which can accept connections on the default HTTP port 80. - * Note: the {@link #start()} method must be called to start accepting connections. - */ - public HTTPServer() { - this(80); - } - - /** - * Sets the port on which this server will accept connections. - * - * @param port the port on which this server will accept connections - */ - public void setPort(int port) { - this.port = port; - } - - /** - * Sets the factory used to create the server socket. - * If null or not set, the default {@link ServerSocketFactory#getDefault()} is used. - * For secure sockets (HTTPS), use an SSLServerSocketFactory instance. - * The port should usually also be changed for HTTPS, e.g. port 443 instead of 80. - *

- * If using the default SSLServerSocketFactory returned by - * {@link SSLServerSocketFactory#getDefault()}, the appropriate system properties - * must be set to configure the default JSSE provider, such as - * {@code javax.net.ssl.keyStore} and {@code javax.net.ssl.keyStorePassword}. - * - * @param factory the server socket factory to use - */ - public void setServerSocketFactory(ServerSocketFactory factory) { - this.serverSocketFactory = factory; - this.secure = factory instanceof SSLServerSocketFactory; - } - - /** - * Sets the socket timeout for established connections. - * - * @param timeout the socket timeout in milliseconds - */ - public void setSocketTimeout(int timeout) { this.socketTimeout = timeout; } - - /** - * Sets the executor used in servicing HTTP connections. - * If null, a default executor is used. The caller is responsible - * for shutting down the provided executor when necessary. - * - * @param executor the executor to use - */ - public void setExecutor(Executor executor) { - this.executor = executor; - } - - /** - * Returns the virtual host with the given name. - * - * @param name the name of the virtual host to return, or null for - * the default virtual host - * @return the virtual host with the given name, or null if it doesn't exist - */ - public VirtualHost getVirtualHost(String name) { - return hosts.get(name == null ? VirtualHost.DEFAULT_HOST_NAME : name); - } - - /** - * Returns all virtual hosts. - * - * @return all virtual hosts (as an unmodifiable set) - */ - public Set getVirtualHosts() { - return Collections.unmodifiableSet(new HashSet(hosts.values())); - } - - /** - * Adds the given virtual host to the server. - * If the host's name or aliases already exist, they are overwritten. - * - * @param host the virtual host to add - */ - public void addVirtualHost(VirtualHost host) { - String name = host.getName(); - hosts.put(name == null ? VirtualHost.DEFAULT_HOST_NAME : name, host); - } - - /** - * Creates the server socket used to accept connections, using the configured - * {@link #setServerSocketFactory ServerSocketFactory} and {@link #setPort port}. - *

- * Cryptic errors seen here often mean the factory configuration details are wrong. - * - * @return the created server socket - * @throws IOException if the socket cannot be created - */ - protected ServerSocket createServerSocket() throws IOException { - ServerSocket serv = serverSocketFactory.createServerSocket(); - serv.setReuseAddress(true); - serv.bind(new InetSocketAddress(port)); - return serv; - } - - /** - * Starts this server. If it is already started, does nothing. - * Note: Once the server is started, configuration-altering methods - * of the server and its virtual hosts must not be used. To modify the - * configuration, the server must first be stopped. - * - * @throws IOException if the server cannot begin accepting connections - */ - public synchronized void start() throws IOException { - if (serv != null) - return; - if (serverSocketFactory == null) // assign default server socket factory if needed - serverSocketFactory = ServerSocketFactory.getDefault(); // plain sockets - serv = createServerSocket(); - if (executor == null) // assign default executor if needed - executor = Executors.newCachedThreadPool(); // consumes no resources when idle - // register all host aliases (which may have been modified) - for (VirtualHost host : getVirtualHosts()) - for (String alias : host.getAliases()) - hosts.put(alias, host); - // start handling incoming connections - new SocketHandlerThread().start(); - } - - /** - * Stops this server. If it is already stopped, does nothing. - * Note that if an {@link #setExecutor Executor} was set, it must be closed separately. - */ - public synchronized void stop() { - try { - if (serv != null) - serv.close(); - } catch (IOException ignore) {} - serv = null; - } - - /** - * Handles communications for a single connection over the given streams. - * Multiple subsequent transactions are handled on the connection, - * until the streams are closed, an error occurs, or the request - * contains a "Connection: close" header which explicitly requests - * the connection be closed after the transaction ends. - * - * @param in the stream from which the incoming requests are read - * @param out the stream into which the outgoing responses are written - * @throws IOException if an error occurs - */ - protected void handleConnection(InputStream in, OutputStream out) throws IOException { - in = new BufferedInputStream(in, 4096); - out = new BufferedOutputStream(out, 4096); - Request req; - Response resp; - do { - // create request and response and handle transaction - req = null; - resp = new Response(out); - try { - req = new Request(in); - handleTransaction(req, resp); - } catch (Throwable t) { // unhandled errors (not normal error responses like 404) - if (req == null) { // error reading request - if (t instanceof IOException && t.getMessage().contains("missing request line")) - break; // we're not in the middle of a transaction - so just disconnect - resp.getHeaders().add("Connection", "close"); // about to close connection - if (t instanceof InterruptedIOException) // e.g. SocketTimeoutException - resp.sendError(408, "Timeout waiting for client request"); - else - resp.sendError(400, "Invalid request: " + t.getMessage()); - } else if (!resp.headersSent()) { // if headers were not already sent, we can send an error response - resp = new Response(out); // ignore whatever headers may have already been set - resp.getHeaders().add("Connection", "close"); // about to close connection - resp.sendError(500, "Error processing request: " + t.getMessage()); - } // otherwise just abort the connection since we can't recover - break; // proceed to close connection - } finally { - resp.close(); // close response and flush output - } - // consume any leftover body data so next request can be processed - transfer(req.getBody(), null, -1); - // RFC7230#6.6: persist connection unless client or server close explicitly (or legacy client) - } while (!"close".equalsIgnoreCase(req.getHeaders().get("Connection")) - && !"close".equalsIgnoreCase(resp.getHeaders().get("Connection")) && req.getVersion().endsWith("1.1")); - } - - /** - * Handles a single transaction on a connection. - * - * Subclasses can override this method to perform filtering on the - * request or response, apply wrappers to them, or further customize - * the transaction processing in some other way. - * - * @param req the transaction request - * @param resp the transaction response (into which the response is written) - * @throws IOException if and error occurs - */ - protected void handleTransaction(Request req, Response resp) throws IOException { - resp.setClientCapabilities(req); - if (preprocessTransaction(req, resp)) - handleMethod(req, resp); - } - - /** - * Preprocesses a transaction, performing various validation checks - * and required special header handling, possibly returning an - * appropriate response. - * - * @param req the request - * @param resp the response - * @return whether further processing should be performed on the transaction - * @throws IOException if an error occurs - */ - protected boolean preprocessTransaction(Request req, Response resp) throws IOException { - Headers reqHeaders = req.getHeaders(); - // validate request - String version = req.getVersion(); - if (version.equals("HTTP/1.1")) { - if (!reqHeaders.contains("Host")) { - // RFC2616#14.23: missing Host header gets 400 - resp.sendError(400, "Missing required Host header"); - return false; - } - // return a continue response before reading body - String expect = reqHeaders.get("Expect"); - if (expect != null) { - if (expect.equalsIgnoreCase("100-continue")) { - Response tempResp = new Response(resp.getOutputStream()); - tempResp.sendHeaders(100); - resp.getOutputStream().flush(); - } else { - // RFC2616#14.20: if unknown expect, send 417 - resp.sendError(417); - return false; - } - } - } else if (version.equals("HTTP/1.0") || version.equals("HTTP/0.9")) { - // RFC2616#14.10 - remove connection headers from older versions - for (String token : splitElements(reqHeaders.get("Connection"), false)) - reqHeaders.remove(token); - } else { - resp.sendError(400, "Unknown version: " + version); - return false; - } - return true; - } - - /** - * Handles a transaction according to the request method. - * - * @param req the transaction request - * @param resp the transaction response (into which the response is written) - * @throws IOException if and error occurs - */ - protected void handleMethod(Request req, Response resp) throws IOException { - String method = req.getMethod(); - Map handlers = req.getContext().getHandlers(); - // RFC 2616#5.1.1 - GET and HEAD must be supported - if (method.equals("GET") || handlers.containsKey(method)) { - serve(req, resp); // method is handled by context handler (or 404) - } else if (method.equals("HEAD")) { // default HEAD handler - req.method = "GET"; // identical to a GET - resp.setDiscardBody(true); // process normally but discard body - serve(req, resp); - } else if (method.equals("TRACE")) { // default TRACE handler - handleTrace(req, resp); - } else { - Set methods = new LinkedHashSet(); - methods.addAll(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); // built-in methods - // "*" is a special server-wide (no-context) request supported by OPTIONS - boolean isServerOptions = req.getPath().equals("*") && method.equals("OPTIONS"); - methods.addAll(isServerOptions ? req.getVirtualHost().getMethods() : handlers.keySet()); - resp.getHeaders().add("Allow", join(", ", methods)); - if (method.equals("OPTIONS")) { // default OPTIONS handler - resp.getHeaders().add("Content-Length", "0"); // RFC2616#9.2 - resp.sendHeaders(200); - } else if (req.getVirtualHost().getMethods().contains(method)) { - resp.sendHeaders(405); // supported by server, but not this context (nor built-in) - } else { - resp.sendError(501); // unsupported method - } - } - } - - /** - * Handles a TRACE method request. - * - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - public void handleTrace(Request req, Response resp) throws IOException { - resp.sendHeaders(200, -1, -1, null, "message/http", null); - OutputStream out = resp.getBody(); - out.write(getBytes("TRACE ", req.getURI().toString(), " ", req.getVersion())); - out.write(CRLF); - req.getHeaders().writeTo(out); - transfer(req.getBody(), out, -1); - } - - /** - * Serves the content for a request by invoking the context - * handler for the requested context (path) and HTTP method. - * - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - protected void serve(Request req, Response resp) throws IOException { - // get context handler to handle request - ContextHandler handler = req.getContext().getHandlers().get(req.getMethod()); - if (handler == null) { - resp.sendError(404); - return; - } - // serve request - int status = 404; - // add directory index if necessary - String path = req.getPath(); - if (path.endsWith("/")) { - String index = req.getVirtualHost().getDirectoryIndex(); - if (index != null) { - req.setPath(path + index); - status = handler.serve(req, resp); - req.setPath(path); - } - } - if (status == 404) - status = handler.serve(req, resp); - if (status > 0) - resp.sendError(status); - } - - /** - * Adds a Content-Type mapping for the given path suffixes. - * If any of the path suffixes had a previous Content-Type associated - * with it, it is replaced with the given one. Path suffixes are - * considered case-insensitive, and contentType is converted to lowercase. - * - * @param contentType the content type (MIME type) to be associated with - * the given path suffixes - * @param suffixes the path suffixes which will be associated with - * the contentType, e.g. the file extensions of served files - * (excluding the '.' character) - */ - public static void addContentType(String contentType, String... suffixes) { - for (String suffix : suffixes) - contentTypes.put(suffix.toLowerCase(Locale.US), contentType.toLowerCase(Locale.US)); - } - - /** - * Adds Content-Type mappings from a standard mime.types file. - * - * @param mimeTypes a mime.types file - * @throws IOException if an error occurs - * @throws FileNotFoundException if the file is not found or cannot be read - */ - public static void addContentTypes(File mimeTypes) throws IOException { - InputStream in = new FileInputStream(mimeTypes); - try { - while (true) { - String line = readLine(in).trim(); // throws EOFException when done - if (line.length() > 0 && line.charAt(0) != '#') { - String[] tokens = split(line, " \t", -1); - for (int i = 1; i < tokens.length; i++) - addContentType(tokens[0], tokens[i]); - } - } - } catch (EOFException ignore) { // the end of file was reached - it's ok - } finally { - in.close(); - } - } - - /** - * Returns the content type for the given path, according to its suffix, - * or the given default content type if none can be determined. - * - * @param path the path whose content type is requested - * @param def a default content type which is returned if none can be - * determined - * @return the content type for the given path, or the given default - */ - public static String getContentType(String path, String def) { - int dot = path.lastIndexOf('.'); - String type = dot < 0 ? def : contentTypes.get(path.substring(dot + 1).toLowerCase(Locale.US)); - return type != null ? type : def; - } - - /** - * Checks whether data of the given content type (MIME type) is compressible. - * - * @param contentType the content type - * @return true if the data is compressible, false if not - */ - public static boolean isCompressible(String contentType) { - int pos = contentType.indexOf(';'); // exclude params - String ct = pos < 0 ? contentType : contentType.substring(0, pos); - for (String s : compressibleContentTypes) - if (s.equals(ct) || s.charAt(0) == '*' && ct.endsWith(s.substring(1)) - || s.charAt(s.length() - 1) == '*' && ct.startsWith(s.substring(0, s.length() - 1))) - return true; - return false; - } - - /** - * Returns the local host's auto-detected name. - * - * @return the local host name - */ - public static String detectLocalHostName() { - try { - return InetAddress.getLocalHost().getCanonicalHostName(); - } catch (UnknownHostException uhe) { - return "localhost"; - } - } - - /** - * Parses name-value pair parameters from the given "x-www-form-urlencoded" - * MIME-type string. This is the encoding used both for parameters passed - * as the query of an HTTP GET method, and as the content of HTML forms - * submitted using the HTTP POST method (as long as they use the default - * "application/x-www-form-urlencoded" encoding in their ENCTYPE attribute). - * UTF-8 encoding is assumed. - *

- * The parameters are returned as a list of string arrays, each containing - * the parameter name as the first element and its corresponding value - * as the second element (or an empty string if there is no value). - *

- * The list retains the original order of the parameters. - * - * @param s an "application/x-www-form-urlencoded" string - * @return the parameter name-value pairs parsed from the given string, - * or an empty list if there are none - */ - public static List parseParamsList(String s) { - if (s == null || s.length() == 0) - return Collections.emptyList(); - List params = new ArrayList(8); - for (String pair : split(s, "&", -1)) { - int pos = pair.indexOf('='); - String name = pos < 0 ? pair : pair.substring(0, pos); - String val = pos < 0 ? "" : pair.substring(pos + 1); - try { - name = URLDecoder.decode(name.trim(), "UTF-8"); - val = URLDecoder.decode(val.trim(), "UTF-8"); - if (name.length() > 0) - params.add(new String[] { name, val }); - } catch (UnsupportedEncodingException ignore) {} // never thrown - } - return params; - } - - /** - * Converts a collection of pairs of objects (arrays of size two, - * each representing a key and corresponding value) into a Map. - * Duplicate keys are ignored (only the first occurrence of each key is considered). - * The map retains the original collection's iteration order. - * - * @param pairs a collection of arrays, each containing a key and corresponding value - * @param the key type - * @param the value type - * @return a map containing the paired keys and values, or an empty map - */ - @SuppressWarnings("unchecked") - public static Map toMap(Collection pairs) { - if (pairs == null || pairs.isEmpty()) - return Collections.emptyMap(); - Map map = new LinkedHashMap(pairs.size()); - for (Object[] pair : pairs) - if (!map.containsKey(pair[0])) - map.put((K)pair[0], (V)pair[1]); - return map; - } - - /** - * Returns the absolute (zero-based) content range value specified - * by the given range string. If multiple ranges are requested, a single - * range containing all of them is returned. - * - * @param range the string containing the range description - * @param length the full length of the requested resource - * @return the requested range, or null if the range value is invalid - */ - public static long[] parseRange(String range, long length) { - long min = Long.MAX_VALUE; - long max = Long.MIN_VALUE; - try { - for (String token : splitElements(range, false)) { - long start, end; - int dash = token.indexOf('-'); - if (dash == 0) { // suffix range - start = length - parseULong(token.substring(1), 10); - end = length - 1; - } else if (dash == token.length() - 1) { // open range - start = parseULong(token.substring(0, dash), 10); - end = length - 1; - } else { // explicit range - start = parseULong(token.substring(0, dash), 10); - end = parseULong(token.substring(dash + 1), 10); - } - if (end < start) - throw new RuntimeException(); - if (start < min) - min = start; - if (end > max) - max = end; - } - if (max < 0) // no tokens - throw new RuntimeException(); - if (max >= length && min < length) - max = length - 1; - return new long[] { min, max }; // start might be >= length! - } catch (RuntimeException re) { // NFE, IOOBE or explicit RE - return null; // RFC2616#14.35.1 - ignore header if invalid - } - } - - /** - * Parses an unsigned long value. This method behaves the same as calling - * {@link Long#parseLong(String, int)}, but considers the string invalid - * if it starts with an ASCII minus sign ('-') or plus sign ('+'). - * - * @param s the String containing the long representation to be parsed - * @param radix the radix to be used while parsing s - * @return the long represented by s in the specified radix - * @throws NumberFormatException if the string does not contain a parsable - * long, or if it starts with an ASCII minus sign or plus sign - */ - public static long parseULong(String s, int radix) throws NumberFormatException { - long val = Long.parseLong(s, radix); // throws NumberFormatException - if (s.charAt(0) == '-' || s.charAt(0) == '+') - throw new NumberFormatException("invalid digit: " + s.charAt(0)); - return val; - } - - /** - * Parses a date string in one of the supported {@link #DATE_PATTERNS}. - * - * Received date header values must be in one of the following formats: - * Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 - * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 - * Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format - * - * @param time a string representation of a time value - * @return the parsed date value - * @throws IllegalArgumentException if the given string does not contain - * a valid date format in any of the supported formats - */ - public static Date parseDate(String time) { - for (String pattern : DATE_PATTERNS) { - try { - SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.US); - df.setLenient(false); - df.setTimeZone(GMT); - return df.parse(time); - } catch (ParseException ignore) {} - } - throw new IllegalArgumentException("invalid date format: " + time); - } - - /** - * Formats the given time value as a string in RFC 1123 format. - * - * @param time the time in milliseconds since January 1, 1970, 00:00:00 GMT - * @return the given time value as a string in RFC 1123 format - */ - public static String formatDate(long time) { - // this implementation performs far better than SimpleDateFormat instances, and even - // quite better than ThreadLocal SDFs - the server's CPU-bound benchmark gains over 20%! - if (time < -62167392000000L || time > 253402300799999L) - throw new IllegalArgumentException("year out of range (0001-9999): " + time); - char[] s = "DAY, 00 MON 0000 00:00:00 GMT".toCharArray(); // copy the format template - Calendar cal = new GregorianCalendar(GMT, Locale.US); - cal.setTimeInMillis(time); - System.arraycopy(DAYS, 4 * (cal.get(Calendar.DAY_OF_WEEK) - 1), s, 0, 3); - System.arraycopy(MONTHS, 4 * cal.get(Calendar.MONTH), s, 8, 3); - int n = cal.get(Calendar.DATE); s[5] += n / 10; s[6] += n % 10; - n = cal.get(Calendar.YEAR); s[12] += n / 1000; s[13] += n / 100 % 10; - s[14] += n / 10 % 10; s[15] += n % 10; - n = cal.get(Calendar.HOUR_OF_DAY); s[17] += n / 10; s[18] += n % 10; - n = cal.get(Calendar.MINUTE); s[20] += n / 10; s[21] += n % 10; - n = cal.get(Calendar.SECOND); s[23] += n / 10; s[24] += n % 10; - return new String(s); - } - - /** - * Splits the given element list string (comma-separated header value) - * into its constituent non-empty trimmed elements. - * (RFC2616#2.1: element lists are delimited by a comma and optional LWS, - * and empty elements are ignored). - * - * @param list the element list string - * @param lower specifies whether the list elements should be lower-cased - * @return the non-empty elements in the list, or an empty array - */ - public static String[] splitElements(String list, boolean lower) { - return split(lower && list != null ? list.toLowerCase(Locale.US) : list, ",", -1); - } - - /** - * Splits the given string into its constituent non-empty trimmed elements, - * which are delimited by any of the given delimiter characters. - * This is a more direct and efficient implementation than using a regex - * (e.g. String.split()), trimming the elements and removing empty ones. - * - * @param str the string to split - * @param delimiters the characters used as the delimiters between elements - * @param limit if positive, limits the returned array size (remaining of str in last element) - * @return the non-empty elements in the string, or an empty array - */ - public static String[] split(String str, String delimiters, int limit) { - if (str == null) - return new String[0]; - Collection elements = new ArrayList(); - int len = str.length(); - int start = 0; - int end; - while (start < len) { - for (end = --limit == 0 ? len : start; - end < len && delimiters.indexOf(str.charAt(end)) < 0; end++); - String element = str.substring(start, end).trim(); - if (element.length() > 0) - elements.add(element); - start = end + 1; - } - return elements.toArray(new String[elements.size()]); - } - - /** - * Returns a string constructed by joining the string representations of the - * iterated objects (in order), with the delimiter inserted between them. - * - * @param delim the delimiter that is inserted between the joined strings - * @param items the items whose string representations are joined - * @param the item type - * @return the joined string - */ - public static String join(String delim, Iterable items) { - StringBuilder sb = new StringBuilder(); - for (Iterator it = items.iterator(); it.hasNext(); ) - sb.append(it.next()).append(it.hasNext() ? delim : ""); - return sb.toString(); - } - - /** - * Returns the parent of the given path. - * - * @param path the path whose parent is returned (must start with '/') - * @return the parent of the given path (excluding trailing slash), - * or null if given path is the root path - */ - public static String getParentPath(String path) { - path = trimRight(path, '/'); // remove trailing slash - int slash = path.lastIndexOf('/'); - return slash < 0 ? null : path.substring(0, slash); - } - - /** - * Returns the given string with all occurrences of the given character - * removed from its right side. - * - * @param s the string to trim - * @param c the character to remove - * @return the trimmed string - */ - public static String trimRight(String s, char c) { - int len = s.length() - 1; - int end; - for (end = len; end >= 0 && s.charAt(end) == c; end--); - return end == len ? s : s.substring(0, end + 1); - } - - /** - * Returns the given string with all occurrences of the given character - * removed from its left side. - * - * @param s the string to trim - * @param c the character to remove - * @return the trimmed string - */ - public static String trimLeft(String s, char c) { - int len = s.length(); - int start; - for (start = 0; start < len && s.charAt(start) == c; start++); - return start == 0 ? s : s.substring(start); - } - - /** - * Trims duplicate consecutive occurrences of the given character within the - * given string, replacing them with a single instance of the character. - * - * @param s the string to trim - * @param c the character to trim - * @return the given string with duplicate consecutive occurrences of c - * replaced by a single instance of c - */ - public static String trimDuplicates(String s, char c) { - int start = 0; - while ((start = s.indexOf(c, start) + 1) > 0) { - int end; - for (end = start; end < s.length() && s.charAt(end) == c; end++); - if (end > start) - s = s.substring(0, start) + s.substring(end); - } - return s; - } - - /** - * Returns a human-friendly string approximating the given data size, - * e.g. "316", "1.8K", "324M", etc. - * - * @param size the size to display - * @return a human-friendly string approximating the given data size - */ - public static String toSizeApproxString(long size) { - final char[] units = { ' ', 'K', 'M', 'G', 'T', 'P', 'E' }; - int u; - double s; - for (u = 0, s = size; s >= 1000; u++, s /= 1024); - return String.format(s < 10 ? "%.1f%c" : "%.0f%c", s, units[u]); - } - - /** - * Returns an HTML-escaped version of the given string for safe display - * within a web page. The characters '&', '>' and '<' must always - * be escaped, and single and double quotes must be escaped within - * attribute values; this method escapes them always. This method can - * be used for generating both HTML and XHTML valid content. - * - * @param s the string to escape - * @return the escaped string - * @see The W3C FAQ - */ - public static String escapeHTML(String s) { - int len = s.length(); - StringBuilder sb = new StringBuilder(len + 30); - int start = 0; - for (int i = 0; i < len; i++) { - String ref = null; - switch (s.charAt(i)) { - case '&': ref = "&"; break; - case '>': ref = ">"; break; - case '<': ref = "<"; break; - case '"': ref = """; break; - case '\'': ref = "'"; break; - } - if (ref != null) { - sb.append(s.substring(start, i)).append(ref); - start = i + 1; - } - } - return start == 0 ? s : sb.append(s.substring(start)).toString(); - } - - /** - * Converts strings to bytes by casting the chars to bytes. - * This is a fast way to encode a string as ISO-8859-1/US-ASCII bytes. - * If multiple strings are provided, their bytes are concatenated. - * - * @param strings the strings to convert (containing only ISO-8859-1 chars) - * @return the byte array - */ - public static byte[] getBytes(String... strings) { - int n = 0; - for (String s : strings) - n += s.length(); - byte[] dest = new byte[n]; - n = 0; - for (String s : strings) - for (int i = 0, len = s.length(); i < len; i++) - dest[n++] = (byte)s.charAt(i); - return dest; - } - - /** - * Transfers data from an input stream to an output stream. - * - * @param in the input stream to transfer from - * @param out the output stream to transfer to (or null to discard output) - * @param len the number of bytes to transfer. If negative, the entire - * contents of the input stream are transferred. - * @throws IOException if an IO error occurs or the input stream ends - * before the requested number of bytes have been read - */ - public static void transfer(InputStream in, OutputStream out, long len) throws IOException { - if (len == 0 || out == null && len < 0 && in.read() < 0) - return; // small optimization - avoid buffer creation - byte[] buf = new byte[4096]; - while (len != 0) { - int count = len < 0 || buf.length < len ? buf.length : (int)len; - count = in.read(buf, 0, count); - if (count < 0) { - if (len > 0) - throw new IOException("unexpected end of stream"); - break; - } - if (out != null) - out.write(buf, 0, count); - len -= len > 0 ? count : 0; - } - } - - /** - * Reads the token starting at the current stream position and ending at - * the first occurrence of the given delimiter byte, in the given encoding. - * - * @param in the stream from which the token is read - * @param delim the byte value which marks the end of the token, - * or -1 if the token ends at the end of the stream - * @param enc a character-encoding name - * @param maxLength the maximum length (in bytes) to read - * @return the read token, excluding the delimiter - * @throws UnsupportedEncodingException if the encoding is not supported - * @throws EOFException if the stream end is reached before a delimiter is found - * @throws IOException if an IO error occurs, or the maximum length - * is reached before the token end is reached - */ - public static String readToken(InputStream in, int delim, - String enc, int maxLength) throws IOException { - // note: we avoid using a ByteArrayOutputStream here because it - // suffers the overhead of synchronization for each byte written - int buflen = maxLength < 512 ? maxLength : 512; // start with less - byte[] buf = new byte[buflen]; - int count = 0; - int c; - while ((c = in.read()) != -1 && c != delim) { - if (count == buflen) { // expand buffer - if (count == maxLength) - throw new IOException("token too large (" + count + ")"); - buflen = maxLength < 2 * buflen ? maxLength : 2 * buflen; - byte[] expanded = new byte[buflen]; - System.arraycopy(buf, 0, expanded, 0, count); - buf = expanded; - } - buf[count++] = (byte)c; - } - if (c < 0 && delim != -1) - throw new EOFException("unexpected end of stream"); - return new String(buf, 0, count, enc); - } - - /** - * Reads the ISO-8859-1 encoded string starting at the current stream - * position and ending at the first occurrence of the LF character. - * - * @param in the stream from which the line is read - * @return the read string, excluding the terminating LF character - * and (if exists) the CR character immediately preceding it - * @throws EOFException if the stream end is reached before an LF character is found - * @throws IOException if an IO error occurs, or the line is longer than 8192 bytes - * @see #readToken(InputStream, int, String, int) - */ - public static String readLine(InputStream in) throws IOException { - String s = readToken(in, '\n', "ISO8859_1", 8192); - return s.length() > 0 && s.charAt(s.length() - 1) == '\r' - ? s.substring(0, s.length() - 1) : s; - } - - /** - * Reads headers from the given stream. Headers are read according to the - * RFC, including folded headers, element lists, and multiple headers - * (which are concatenated into a single element list header). - * Leading and trailing whitespace is removed. - * - * @param in the stream from which the headers are read - * @return the read headers (possibly empty, if none exist) - * @throws IOException if an IO error occurs or the headers are malformed - * or there are more than 100 header lines - */ - public static Headers readHeaders(InputStream in) throws IOException { - Headers headers = new Headers(); - String line; - String prevLine = ""; - int count = 0; - while ((line = readLine(in)).length() > 0) { - int first; - for (first = 0; first < line.length() && - Character.isWhitespace(line.charAt(first)); first++); - if (first > 0) // unfold header continuation line - line = prevLine + ' ' + line.substring(first); - int separator = line.indexOf(':'); - if (separator < 0) - throw new IOException("invalid header: \"" + line + "\""); - String name = line.substring(0, separator); - String value = line.substring(separator + 1).trim(); // ignore LWS - Header replaced = headers.replace(name, value); - // concatenate repeated headers (distinguishing repeated from folded) - if (replaced != null && first == 0) { - value = replaced.getValue() + ", " + value; - line = name + ": " + value; - headers.replace(name, value); - } - prevLine = line; - if (++count > 100) - throw new IOException("too many header lines"); - } - return headers; - } - - /** - * Matches the given ETag value against the given ETags. A match is found - * if the given ETag is not null, and either the ETags contain a "*" value, - * or one of them is identical to the given ETag. If strong comparison is - * used, tags beginning with the weak ETag prefix "W/" never match. - * See RFC2616#3.11, RFC2616#13.3.3. - * - * @param strong if true, strong comparison is used, otherwise weak - * comparison is used - * @param etags the ETags to match against - * @param etag the ETag to match - * @return true if the ETag is matched, false otherwise - */ - public static boolean match(boolean strong, String[] etags, String etag) { - if (etag == null || strong && etag.startsWith("W/")) - return false; - for (String e : etags) - if (e.equals("*") || (e.equals(etag) && !(strong && (e.startsWith("W/"))))) - return true; - return false; - } - - /** - * Calculates the appropriate response status for the given request and - * its resource's last-modified time and ETag, based on the conditional - * headers present in the request. - * - * @param req the request - * @param lastModified the resource's last modified time - * @param etag the resource's ETag - * @return the appropriate response status for the request - */ - public static int getConditionalStatus(Request req, long lastModified, String etag) { - Headers headers = req.getHeaders(); - // If-Match - String header = headers.get("If-Match"); - if (header != null && !match(true, splitElements(header, false), etag)) - return 412; - // If-Unmodified-Since - Date date = headers.getDate("If-Unmodified-Since"); - if (date != null && lastModified > date.getTime()) - return 412; - // If-Modified-Since - int status = 200; - boolean force = false; - date = headers.getDate("If-Modified-Since"); - if (date != null && date.getTime() <= System.currentTimeMillis()) { - if (lastModified > date.getTime()) - force = true; - else - status = 304; - } - // If-None-Match - header = headers.get("If-None-Match"); - if (header != null) { - if (match(false, splitElements(header, false), etag)) // RFC7232#3.2: use weak matching - status = req.getMethod().equals("GET") - || req.getMethod().equals("HEAD") ? 304 : 412; - else - force = true; - } - return force ? 200 : status; - } - - /** - * Serves a context's contents from a file based resource. - * - * The file is located by stripping the given context prefix from - * the request's path, and appending the result to the given base directory. - * - * Missing, forbidden and otherwise invalid files return the appropriate - * error response. Directories are served as an HTML index page if the - * virtual host allows one, or a forbidden error otherwise. Files are - * sent with their corresponding content types, and handle conditional - * and partial retrievals according to the RFC. - * - * @param base the base directory to which the context is mapped - * @param context the context which is mapped to the base directory - * @param req the request - * @param resp the response into which the content is written - * @return the HTTP status code to return, or 0 if a response was sent - * @throws IOException if an error occurs - */ - public static int serveFile(File base, String context, - Request req, Response resp) throws IOException { - String relativePath = req.getPath().substring(context.length()); - File file = new File(base, relativePath).getCanonicalFile(); - if (!file.exists() || file.isHidden() || file.getName().startsWith(".")) { - return 404; - } else if (!file.canRead() || !file.getPath().startsWith(base.getPath())) { // validate - return 403; - } else if (file.isDirectory()) { - if (relativePath.endsWith("/")) { - if (!req.getVirtualHost().isAllowGeneratedIndex()) - return 403; - resp.send(200, createIndex(file, req.getPath())); - } else { // redirect to the normalized directory URL ending with '/' - resp.redirect(req.getBaseURL() + req.getPath() + "/", true); - } - } else if (relativePath.endsWith("/")) { - return 404; // non-directory ending with slash (File constructor removed it) - } else { - serveFileContent(file, req, resp); - } - return 0; - } - - /** - * Serves the contents of a file, with its corresponding content type, - * last modification time, etc. conditional and partial retrievals are - * handled according to the RFC. - * - * @param file the existing and readable file whose contents are served - * @param req the request - * @param resp the response into which the content is written - * @throws IOException if an error occurs - */ - public static void serveFileContent(File file, Request req, Response resp) throws IOException { - long len = file.length(); - long lastModified = file.lastModified(); - String etag = "W/\"" + lastModified + "\""; // a weak tag based on date - int status = 200; - // handle range or conditional request - long[] range = req.getRange(len); - if (range == null || len == 0) { - status = getConditionalStatus(req, lastModified, etag); - } else { - String ifRange = req.getHeaders().get("If-Range"); - if (ifRange == null) { - if (range[0] >= len) - status = 416; // unsatisfiable range - else - status = getConditionalStatus(req, lastModified, etag); - } else if (range[0] >= len) { - // RFC2616#14.16, 10.4.17: invalid If-Range gets everything - range = null; - } else { // send either range or everything - if (!ifRange.startsWith("\"") && !ifRange.startsWith("W/")) { - Date date = req.getHeaders().getDate("If-Range"); - if (date != null && lastModified > date.getTime()) - range = null; // modified - send everything - } else if (!ifRange.equals(etag)) { - range = null; // modified - send everything - } - } - } - // send the response - Headers respHeaders = resp.getHeaders(); - switch (status) { - case 304: // no other headers or body allowed - respHeaders.add("ETag", etag); - respHeaders.add("Vary", "Accept-Encoding"); - respHeaders.add("Last-Modified", formatDate(lastModified)); - resp.sendHeaders(304); - break; - case 412: - resp.sendHeaders(412); - break; - case 416: - respHeaders.add("Content-Range", "bytes */" + len); - resp.sendHeaders(416); - break; - case 200: - // send OK response - resp.sendHeaders(200, len, lastModified, etag, - getContentType(file.getName(), "application/octet-stream"), range); - // send body - InputStream in = new FileInputStream(file); - try { - resp.sendBody(in, len, range); - } finally { - in.close(); - } - break; - default: - resp.sendHeaders(500); // should never happen - break; - } - } - - /** - * Serves the contents of a directory as an HTML file index. - * - * @param dir the existing and readable directory whose contents are served - * @param path the displayed base path corresponding to dir - * @return an HTML string containing the file index for the directory - */ - public static String createIndex(File dir, String path) { - if (!path.endsWith("/")) - path += "/"; - // calculate name column width - int w = 21; // minimum width - for (String name : dir.list()) - if (name.length() > w) - w = name.length(); - w += 2; // with room for added slash and space - // note: we use apache's format, for consistent user experience - Formatter f = new Formatter(Locale.US); - f.format("%n" + - "Index of %s%n" + - "

Index of %s

%n" + - "
 Name%" + (w - 5) + "s Last modified      Size
", - path, path, ""); - if (path.length() > 1) // add parent link if not root path - f.format(" Parent Directory%" - + (w + 5) + "s-%n", getParentPath(path), ""); - for (File file : dir.listFiles()) { - try { - String name = file.getName() + (file.isDirectory() ? "/" : ""); - String size = file.isDirectory() ? "- " : toSizeApproxString(file.length()); - // properly url-encode the link - String link = new URI(null, path + name, null).toASCIIString(); - if (!file.isHidden() && !name.startsWith(".")) - f.format(" %s%-" + (w - name.length()) + - "s‎%td-%"); - return f.toString(); - } - - /** - * Starts a stand-alone HTTP server, serving files from disk. - * - * @param args the command line arguments - */ - // public static void main(String[] args) { - // try { - // if (args.length == 0) { - // System.err.printf("Usage: java [-options] %s [port]%n" + - // "To enable SSL: specify options -Djavax.net.ssl.keyStore, " + - // "-Djavax.net.ssl.keyStorePassword, etc.%n", HTTPServer.class.getName()); - // return; - // } - // File dir = new File(args[0]); - // if (!dir.canRead()) - // throw new FileNotFoundException(dir.getAbsolutePath()); - // int port = args.length < 2 ? 80 : Integer.parseInt(args[1]); - // // set up server - // for (File f : Arrays.asList(new File("/etc/mime.types"), new File(dir, ".mime.types"))) - // if (f.exists()) - // addContentTypes(f); - // HTTPServer server = new HTTPServer(port); - // if (System.getProperty("javax.net.ssl.keyStore") != null) // enable SSL if configured - // server.setServerSocketFactory(SSLServerSocketFactory.getDefault()); - // VirtualHost host = server.getVirtualHost(null); // default host - // host.setAllowGeneratedIndex(true); // with directory index pages - // host.addContext("/", new FileContextHandler(dir)); - // host.addContext("/api/time", new ContextHandler() { - // public int serve(Request req, Response resp) throws IOException { - // long now = System.currentTimeMillis(); - // resp.getHeaders().add("Content-Type", "text/plain"); - // resp.send(200, String.format("%tF % new FilterChallenge) + val filters = Map("FilterChallenge" -> new FilterChallenge, + "FontFunCaptcha" -> new FontFunCaptcha, + "GifCaptcha" -> new GifCaptcha, + "ShadowTextCaptcha" -> new ShadowTextCaptcha + ) def getCaptcha(id: Id): Array[Byte] = { imagePstmt.setString(1, id.id) @@ -38,12 +45,13 @@ class Captcha { def getChallenge(param: Parameters): Id = { //TODO: eval params to choose a provider - val providerMap = "FilterChallenge" + val providerMap = "GifCaptcha" val provider = filters(providerMap) val challenge = provider.returnChallenge() val blob = new ByteArrayInputStream(challenge.content) val token = scala.util.Random.nextInt(10000).toString val id = Id(token) + print("Successfull") insertPstmt.setString(1, token) insertPstmt.setString(2, provider.getId) insertPstmt.setString(3, challenge.secret)