diff options
Diffstat (limited to 'core')
33 files changed, 3955 insertions, 1837 deletions
diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..868a6b2 --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,2 @@ +/.settings/ +/LICENSE.txt diff --git a/core/pom.xml b/core/pom.xml index e82d9d0..0e4762d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -1,83 +1,28 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>fi.iki.elonen</groupId> - <artifactId>nanohttpd</artifactId> - <version>2.1.0</version> - <packaging>jar</packaging> - - <name>NanoHttpd-Core</name> - <url>https://github.com/NanoHttpd/nanohttpd</url> - - <dependencies> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.8.2</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpclient</artifactId> - <version>4.2.5</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpmime</artifactId> - <version>4.2.5</version> - <scope>test</scope> - </dependency> - </dependencies> - - <build> - <extensions> - <extension> - <groupId>org.jvnet.wagon-svn</groupId> - <artifactId>wagon-svn</artifactId> - <version>1.8</version> - </extension> - <extension> - <groupId>org.apache.maven.wagon</groupId> - <artifactId>wagon-ftp</artifactId> - <version>1.0-alpha-6</version> - </extension> - </extensions> - - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-source-plugin</artifactId> - <version>2.2.1</version> - <executions> - <execution> - <id>attach-sources</id> - <goals> - <goal>jar</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-release-plugin</artifactId> - <version>2.4</version> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-javadoc-plugin</artifactId> - <version>2.9</version> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>2.3.1</version> - <configuration> - <source>1.6</source> - <target>1.6</target> - </configuration> - </plugin> - </plugins> - </build> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.nanohttpd</groupId> + <artifactId>nanohttpd-project</artifactId> + <version>2.2.0</version> + </parent> + <artifactId>nanohttpd</artifactId> + <packaging>jar</packaging> + <name>NanoHttpd-Core</name> + <dependencies> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.2.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpmime</artifactId> + <version>4.2.5</version> + <scope>test</scope> + </dependency> + </dependencies> + <properties> + <minimal.coverage>0.82</minimal.coverage> + </properties> </project> diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java index 73fea3c..394db9f 100644 --- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java +++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java @@ -1,35 +1,108 @@ package fi.iki.elonen; -import java.io.*; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; +import java.net.URL; import java.net.URLDecoder; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.security.KeyStore; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.Properties; import java.util.StringTokenizer; import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.NanoHTTPD.Response.Status; /** * A simple, tiny, nicely embeddable HTTP server in Java * <p/> * <p/> * NanoHTTPD - * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p> + * <p> + * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias + * </p> * <p/> * <p/> * <b>Features + limitations: </b> @@ -38,8 +111,10 @@ import java.util.TimeZone; * <li>Only one Java file</li> * <li>Java 5 compatible</li> * <li>Released as open source, Modified BSD licence</li> - * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li> - * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li> + * <li>No fixed config files, logging, authorization etc. (Implement yourself if + * you need them.)</li> + * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT + * support in 1.25)</li> * <li>Supports both dynamic content and file serving</li> * <li>Supports file upload (since version 1.2, 2010)</li> * <li>Supports partial content (streaming)</li> @@ -53,8 +128,9 @@ import java.util.TimeZone; * <li>File server does the 301 redirection trick for directories without '/'</li> * <li>File server supports simple skipping for files (continue download)</li> * <li>File server serves also very long files without memory overhead</li> - * <li>Contains a built-in list of most common mime types</li> - * <li>All header names are converted lowercase so they don't vary between browsers/clients</li> + * <li>Contains a built-in list of most common MIME types</li> + * <li>All header names are converted to lower case so they don't vary between + * browsers/clients</li> * <p/> * </ul> * <p/> @@ -66,949 +142,905 @@ import java.util.TimeZone; * <p/> * </ul> * <p/> - * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence) + * See the separate "LICENSE.md" file for the distribution license (Modified BSD + * licence) */ public abstract class NanoHTTPD { - /** - * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) - * This is required as the Keep-Alive HTTP connections would otherwise - * block the socket reading thread forever (or as long the browser is open). - */ - public static final int SOCKET_READ_TIMEOUT = 5000; - /** - * Common mime type for dynamic content: plain text - */ - public static final String MIME_PLAINTEXT = "text/plain"; - /** - * Common mime type for dynamic content: html - */ - public static final String MIME_HTML = "text/html"; - /** - * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing. - */ - private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; - private final String hostname; - private final int myPort; - private ServerSocket myServerSocket; - private Set<Socket> openConnections = new HashSet<Socket>(); - private Thread myThread; + /** * Pluggable strategy for asynchronously executing requests. */ - private AsyncRunner asyncRunner; - /** - * Pluggable strategy for creating and cleaning up temporary files. - */ - private TempFileManagerFactory tempFileManagerFactory; + public interface AsyncRunner { - /** - * Constructs an HTTP server on given port. - */ - public NanoHTTPD(int port) { - this(null, port); + void closeAll(); + + void closed(ClientHandler clientHandler); + + void exec(ClientHandler code); } /** - * Constructs an HTTP server on given hostname and port. + * The runnable that will be used for every new client connection. */ - public NanoHTTPD(String hostname, int port) { - this.hostname = hostname; - this.myPort = port; - setTempFileManagerFactory(new DefaultTempFileManagerFactory()); - setAsyncRunner(new DefaultAsyncRunner()); - } + public class ClientHandler implements Runnable { - private static final void safeClose(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - } + private final InputStream inputStream; + + private final Socket acceptSocket; + + private ClientHandler(InputStream inputStream, Socket acceptSocket) { + this.inputStream = inputStream; + this.acceptSocket = acceptSocket; } - } - private static final void safeClose(Socket closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - } + public void close() { + safeClose(this.inputStream); + safeClose(this.acceptSocket); } - } - private static final void safeClose(ServerSocket closeable) { - if (closeable != null) { + @Override + public void run() { + OutputStream outputStream = null; try { - closeable.close(); - } catch (IOException e) { + outputStream = this.acceptSocket.getOutputStream(); + TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); + HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); + while (!this.acceptSocket.isClosed()) { + session.execute(); + } + } catch (Exception e) { + // When the socket is closed by the client, + // we throw our own SocketException + // to break the "keep alive" loop above. If + // the exception was anything other + // than the expected SocketException OR a + // SocketTimeoutException, print the + // stacktrace + if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { + NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); + } + } finally { + safeClose(outputStream); + safeClose(this.inputStream); + safeClose(this.acceptSocket); + NanoHTTPD.this.asyncRunner.closed(this); } } } - /** - * Start the server. - * - * @throws IOException if the socket is in use. - */ - public void start() throws IOException { - myServerSocket = new ServerSocket(); - myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); - - myThread = new Thread(new Runnable() { - @Override - public void run() { - do { - try { - final Socket finalAccept = myServerSocket.accept(); - registerConnection(finalAccept); - finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT); - final InputStream inputStream = finalAccept.getInputStream(); - asyncRunner.exec(new Runnable() { - @Override - public void run() { - OutputStream outputStream = null; - try { - outputStream = finalAccept.getOutputStream(); - TempFileManager tempFileManager = tempFileManagerFactory.create(); - HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress()); - while (!finalAccept.isClosed()) { - session.execute(); - } - } catch (Exception e) { - // When the socket is closed by the client, we throw our own SocketException - // to break the "keep alive" loop above. - if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) { - e.printStackTrace(); - } - } finally { - safeClose(outputStream); - safeClose(inputStream); - safeClose(finalAccept); - unRegisterConnection(finalAccept); - } - } - }); - } catch (IOException e) { - } - } while (!myServerSocket.isClosed()); - } - }); - myThread.setDaemon(true); - myThread.setName("NanoHttpd Main Listener"); - myThread.start(); - } + public static class Cookie { - /** - * Stop the server. - */ - public void stop() { - try { - safeClose(myServerSocket); - closeAllConnections(); - if (myThread != null) { - myThread.join(); - } - } catch (Exception e) { - e.printStackTrace(); + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); } - } - /** - * Registers that a new connection has been set up. - * - * @param socket the {@link Socket} for the connection. - */ - public synchronized void registerConnection(Socket socket) { - openConnections.add(socket); - } - - /** - * Registers that a connection has been closed - * - * @param socket - * the {@link Socket} for the connection. - */ - public synchronized void unRegisterConnection(Socket socket) { - openConnections.remove(socket); - } + private final String n, v, e; - /** - * Forcibly closes all connections that are open. - */ - public synchronized void closeAllConnections() { - for (Socket socket : openConnections) { - safeClose(socket); + public Cookie(String name, String value) { + this(name, value, 30); } - } - public final int getListeningPort() { - return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); - } + public Cookie(String name, String value, int numDays) { + this.n = name; + this.v = value; + this.e = getHTTPTime(numDays); + } - public final boolean wasStarted() { - return myServerSocket != null && myThread != null; - } + public Cookie(String name, String value, String expires) { + this.n = name; + this.v = value; + this.e = expires; + } - public final boolean isAlive() { - return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive(); + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, this.n, this.v, this.e); + } } /** - * Create a response with known length. - * - * TODO: Remove this implementation when updating to v2.2.0. + * Provides rudimentary support for cookies. Doesn't support 'path', + * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported + * features. + * + * @author LordFokas */ - public static Response newFixedLengthResponse(Response.Status status, String mimeType, String txt) { - return new Response(status, mimeType, txt); - } + public class CookieHandler implements Iterable<String> { - /** - * Override this to customize the server. - * <p/> - * <p/> - * (By default, this delegates to serveFile() and allows directory listing.) - * - * @param uri Percent-decoded URI without parameters, for example "/index.cgi" - * @param method "GET", "POST" etc. - * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. - * @param headers Header entries, percent decoded - * @return HTTP response, see class Response for details - */ - @Deprecated - public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, - Map<String, String> files) { - return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); - } + private final HashMap<String, String> cookies = new HashMap<String, String>(); - /** - * Override this to customize the server. - * <p/> - * <p/> - * (By default, this delegates to serveFile() and allows directory listing.) - * - * @param session The HTTP session - * @return HTTP response, see class Response for details - */ - public Response serve(IHTTPSession session) { - Map<String, String> files = new HashMap<String, String>(); - Method method = session.getMethod(); - if (Method.PUT.equals(method) || Method.POST.equals(method)) { - try { - session.parseBody(files); - } catch (IOException ioe) { - return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - } catch (ResponseException re) { - return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); + private final ArrayList<Cookie> queue = new ArrayList<Cookie>(); + + public CookieHandler(Map<String, String> httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + this.cookies.put(data[0], data[1]); + } + } } } - Map<String, String> parms = session.getParms(); - parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString()); - return serve(session.getUri(), method, session.getHeaders(), parms, files); - } + /** + * Set a cookie with an expiration date from a month ago, effectively + * deleting it on the client side. + * + * @param name + * The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } - /** - * Decode percent encoded <code>String</code> values. - * - * @param str the percent encoded <code>String</code> - * @return expanded form of the input, for example "foo%20bar" becomes "foo bar" - */ - protected String decodePercent(String str) { - String decoded = null; - try { - decoded = URLDecoder.decode(str, "UTF8"); - } catch (UnsupportedEncodingException ignored) { + @Override + public Iterator<String> iterator() { + return this.cookies.keySet().iterator(); } - return decoded; - } - /** - * Decode parameters from a URL, handing the case where a single parameter name might have been - * supplied several times, by return lists of values. In general these lists will contain a single - * element. - * - * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method. - * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). - */ - protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { - return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER)); - } + /** + * Read a cookie from the HTTP Headers. + * + * @param name + * The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return this.cookies.get(name); + } - /** - * Decode parameters from a URL, handing the case where a single parameter name might have been - * supplied several times, by return lists of values. In general these lists will contain a single - * element. - * - * @param queryString a query string pulled from the URL. - * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). - */ - protected Map<String, List<String>> decodeParameters(String queryString) { - Map<String, List<String>> parms = new HashMap<String, List<String>>(); - if (queryString != null) { - StringTokenizer st = new StringTokenizer(queryString, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); - if (!parms.containsKey(propertyName)) { - parms.put(propertyName, new ArrayList<String>()); - } - String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null; - if (propertyValue != null) { - parms.get(propertyName).add(propertyValue); - } + public void set(Cookie cookie) { + this.queue.add(cookie); + } + + /** + * Sets a cookie. + * + * @param name + * The cookie's name. + * @param value + * The cookie's value. + * @param expires + * How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + /** + * Internally used by the webserver to add all queued cookies into the + * Response's HTTP Headers. + * + * @param response + * The Response object to which headers the queued cookies + * will be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : this.queue) { + response.addHeader("Set-Cookie", cookie.getHTTPHeader()); } } - return parms; } - // ------------------------------------------------------------------------------- // - // - // Threading Strategy. - // - // ------------------------------------------------------------------------------- // - /** - * Pluggable strategy for asynchronously executing requests. - * - * @param asyncRunner new strategy for handling threads. + * Default threading strategy for NanoHTTPD. + * <p/> + * <p> + * By default, the server spawns a new Thread for every incoming request. + * These are set to <i>daemon</i> status, and named according to the request + * number. The name is useful when profiling the application. + * </p> */ - public void setAsyncRunner(AsyncRunner asyncRunner) { - this.asyncRunner = asyncRunner; - } + public static class DefaultAsyncRunner implements AsyncRunner { - // ------------------------------------------------------------------------------- // - // - // Temp file handling strategy. - // - // ------------------------------------------------------------------------------- // + private long requestCount; - /** - * Pluggable strategy for creating and cleaning up temporary files. - * - * @param tempFileManagerFactory new strategy for handling temp files. - */ - public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { - this.tempFileManagerFactory = tempFileManagerFactory; - } + private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>()); - /** - * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value. - */ - public enum Method { - GET, PUT, POST, DELETE, HEAD, OPTIONS; + /** + * @return a list with currently running clients. + */ + public List<ClientHandler> getRunning() { + return running; + } - static Method lookup(String method) { - for (Method m : Method.values()) { - if (m.toString().equalsIgnoreCase(method)) { - return m; - } + @Override + public void closeAll() { + // copy of the list for concurrency + for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) { + clientHandler.close(); } - return null; } - } - /** - * Pluggable strategy for asynchronously executing requests. - */ - public interface AsyncRunner { - void exec(Runnable code); - } + @Override + public void closed(ClientHandler clientHandler) { + this.running.remove(clientHandler); + } - /** - * Factory to create temp file managers. - */ - public interface TempFileManagerFactory { - TempFileManager create(); + @Override + public void exec(ClientHandler clientHandler) { + ++this.requestCount; + Thread t = new Thread(clientHandler); + t.setDaemon(true); + t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); + this.running.add(clientHandler); + t.start(); + } } - // ------------------------------------------------------------------------------- // - /** - * Temp file manager. + * Default strategy for creating and cleaning up temporary files. * <p/> - * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup - * temporary files created as a result of handling the request.</p> + * <p> + * By default, files are created by <code>File.createTempFile()</code> in + * the directory specified. + * </p> */ - public interface TempFileManager { - TempFile createTempFile() throws Exception; + public static class DefaultTempFile implements TempFile { - void clear(); - } + private final File file; - /** - * A temp file. - * <p/> - * <p>Temp files are responsible for managing the actual temporary storage and cleaning - * themselves up when no longer needed.</p> - */ - public interface TempFile { - OutputStream open() throws Exception; + private final OutputStream fstream; - void delete() throws Exception; + public DefaultTempFile(File tempdir) throws IOException { + this.file = File.createTempFile("NanoHTTPD-", "", tempdir); + this.fstream = new FileOutputStream(this.file); + } - String getName(); - } + @Override + public void delete() throws Exception { + safeClose(this.fstream); + if (!this.file.delete()) { + throw new Exception("could not delete temporary file"); + } + } - /** - * Default threading strategy for NanoHttpd. - * <p/> - * <p>By default, the server spawns a new Thread for every incoming request. These are set - * to <i>daemon</i> status, and named according to the request number. The name is - * useful when profiling the application.</p> - */ - public static class DefaultAsyncRunner implements AsyncRunner { - private long requestCount; + @Override + public String getName() { + return this.file.getAbsolutePath(); + } @Override - public void exec(Runnable code) { - ++requestCount; - Thread t = new Thread(code); - t.setDaemon(true); - t.setName("NanoHttpd Request Processor (#" + requestCount + ")"); - t.start(); + public OutputStream open() throws Exception { + return this.fstream; } } /** * Default strategy for creating and cleaning up temporary files. * <p/> - * <p></p>This class stores its files in the standard location (that is, - * wherever <code>java.io.tmpdir</code> points to). Files are added - * to an internal list, and deleted when no longer needed (that is, - * when <code>clear()</code> is invoked at the end of processing a - * request).</p> + * <p> + * This class stores its files in the standard location (that is, wherever + * <code>java.io.tmpdir</code> points to). Files are added to an internal + * list, and deleted when no longer needed (that is, when + * <code>clear()</code> is invoked at the end of processing a request). + * </p> */ public static class DefaultTempFileManager implements TempFileManager { - private final String tmpdir; + + private final File tmpdir; + private final List<TempFile> tempFiles; public DefaultTempFileManager() { - tmpdir = System.getProperty("java.io.tmpdir"); - tempFiles = new ArrayList<TempFile>(); - } - - @Override - public TempFile createTempFile() throws Exception { - DefaultTempFile tempFile = new DefaultTempFile(tmpdir); - tempFiles.add(tempFile); - return tempFile; + this.tmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!tmpdir.exists()) { + tmpdir.mkdirs(); + } + this.tempFiles = new ArrayList<TempFile>(); } @Override public void clear() { - for (TempFile file : tempFiles) { + for (TempFile file : this.tempFiles) { try { file.delete(); } catch (Exception ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); } } - tempFiles.clear(); + this.tempFiles.clear(); + } + + @Override + public TempFile createTempFile(String filename_hint) throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); + this.tempFiles.add(tempFile); + return tempFile; } } /** * Default strategy for creating and cleaning up temporary files. - * <p/> - * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in - * the directory specified.</p> */ - public static class DefaultTempFile implements TempFile { - private File file; - private OutputStream fstream; - - public DefaultTempFile(String tempdir) throws IOException { - file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); - fstream = new FileOutputStream(file); - } - - @Override - public OutputStream open() throws Exception { - return fstream; - } - - @Override - public void delete() throws Exception { - safeClose(fstream); - file.delete(); - } + private class DefaultTempFileManagerFactory implements TempFileManagerFactory { @Override - public String getName() { - return file.getAbsolutePath(); + public TempFileManager create() { + return new DefaultTempFileManager(); } } - /** - * HTTP response. Return one of these from serve(). - */ - public static class Response { - /** - * HTTP status code after processing, e.g. "200 OK", HTTP_OK - */ - private IStatus status; - /** - * MIME type of content, e.g. "text/html" - */ - private String mimeType; - /** - * Data of the response, may be null. - */ - private InputStream data; - /** - * Headers for the HTTP response. Use addHeader() to add lines. - */ - private Map<String, String> header = new HashMap<String, String>(); - /** - * The request method that spawned this response. - */ - private Method requestMethod; - /** - * Use chunkedTransfer - */ - private boolean chunkedTransfer; + private static final String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;]*)['|\"]?"; - /** - * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message - */ - public Response(String msg) { - this(Status.OK, MIME_HTML, msg); - } + private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE); - /** - * Basic constructor. - */ - public Response(IStatus status, String mimeType, InputStream data) { - this.status = status; - this.mimeType = mimeType; - this.data = data; - } + private static final String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;]*)['|\"]?"; - /** - * Convenience method that makes an InputStream out of given text. - */ - public Response(IStatus status, String mimeType, String txt) { - this.status = status; - this.mimeType = mimeType; - try { - this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null; - } catch (java.io.UnsupportedEncodingException uee) { - uee.printStackTrace(); - } - } + private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE); - /** - * Adds given line to the header. - */ - public void addHeader(String name, String value) { - header.put(name, value); - } + /** + * Creates a normal ServerSocket for TCP connections + */ + public static class DefaultServerSocketFactory implements ServerSocketFactory { - public String getHeader(String name) { - return header.get(name); + @Override + public ServerSocket create() throws IOException { + return new ServerSocket(); } - /** - * Sends given response to the socket. - */ - protected void send(OutputStream outputStream) { - String mime = mimeType; - SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); - gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); - - try { - if (status == null) { - throw new Error("sendResponse(): Status can't be null."); - } - PrintWriter pw = new PrintWriter(outputStream); - pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); - - if (mime != null) { - pw.print("Content-Type: " + mime + "\r\n"); - } - - if (header == null || header.get("Date") == null) { - pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); - } - - if (header != null) { - for (String key : header.keySet()) { - String value = header.get(key); - pw.print(key + ": " + value + "\r\n"); - } - } + } - sendConnectionHeaderIfNotAlreadyPresent(pw, header); + /** + * Creates a new SSLServerSocket + */ + public static class SecureServerSocketFactory implements ServerSocketFactory { - if (requestMethod != Method.HEAD && chunkedTransfer) { - sendAsChunked(outputStream, pw); - } else { - int pending = data != null ? data.available() : 0; - sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending); - pw.print("\r\n"); - pw.flush(); - sendAsFixedLength(outputStream, pending); - } - outputStream.flush(); - safeClose(data); - } catch (IOException ioe) { - // Couldn't write? No can do. - } - } + private SSLServerSocketFactory sslServerSocketFactory; - protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) { - if (!headerAlreadySent(header, "content-length")) { - pw.print("Content-Length: "+ size +"\r\n"); - } - } + private String[] sslProtocols; - protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) { - if (!headerAlreadySent(header, "connection")) { - pw.print("Connection: keep-alive\r\n"); - } + public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.sslServerSocketFactory = sslServerSocketFactory; + this.sslProtocols = sslProtocols; } - private boolean headerAlreadySent(Map<String, String> header, String name) { - boolean alreadySent = false; - for (String headerName : header.keySet()) { - alreadySent |= headerName.equalsIgnoreCase(name); + @Override + public ServerSocket create() throws IOException { + SSLServerSocket ss = null; + ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); + if (this.sslProtocols != null) { + ss.setEnabledProtocols(this.sslProtocols); + } else { + ss.setEnabledProtocols(ss.getSupportedProtocols()); } - return alreadySent; + ss.setUseClientMode(false); + ss.setWantClientAuth(false); + ss.setNeedClientAuth(false); + return ss; } - private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { - pw.print("Transfer-Encoding: chunked\r\n"); - pw.print("\r\n"); - pw.flush(); - int BUFFER_SIZE = 16 * 1024; - byte[] CRLF = "\r\n".getBytes(); - byte[] buff = new byte[BUFFER_SIZE]; - int read; - while ((read = data.read(buff)) > 0) { - outputStream.write(String.format("%x\r\n", read).getBytes()); - outputStream.write(buff, 0, read); - outputStream.write(CRLF); - } - outputStream.write(String.format("0\r\n\r\n").getBytes()); - } + } - private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException { - if (requestMethod != Method.HEAD && data != null) { - int BUFFER_SIZE = 16 * 1024; - byte[] buff = new byte[BUFFER_SIZE]; - while (pending > 0) { - int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); - if (read <= 0) { - break; - } - outputStream.write(buff, 0, read); - pending -= read; - } - } - } + private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; - public IStatus getStatus() { - return status; - } + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); - public void setStatus(Status status) { - this.status = status; - } + private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; - public String getMimeType() { - return mimeType; - } + private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } + private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; - public InputStream getData() { - return data; - } + private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); - public void setData(InputStream data) { - this.data = data; - } + protected class HTTPSession implements IHTTPSession { - public Method getRequestMethod() { - return requestMethod; - } + private static final int REQUEST_BUFFER_LEN = 512; - public void setRequestMethod(Method requestMethod) { - this.requestMethod = requestMethod; - } + private static final int MEMORY_STORE_LIMIT = 1024; - public void setChunkedTransfer(boolean chunkedTransfer) { - this.chunkedTransfer = chunkedTransfer; - } + public static final int BUFSIZE = 8192; - public interface IStatus { - int getRequestStatus(); - String getDescription(); - } + public static final int MAX_HEADER_SIZE = 1024; - /** - * Some HTTP response status codes - */ - public enum Status implements IStatus { - SWITCH_PROTOCOL(101, "Switching Protocols"), + private final TempFileManager tempFileManager; - OK(200, "OK"), - CREATED(201, "Created"), - ACCEPTED(202, "Accepted"), - NO_CONTENT(204, "No Content"), - PARTIAL_CONTENT(206, "Partial Content"), + private final OutputStream outputStream; - REDIRECT(301, "Moved Permanently"), - TEMPORARY_REDIRECT(302, "Moved Temporarily"), - NOT_MODIFIED(304, "Not Modified"), + private final BufferedInputStream inputStream; - BAD_REQUEST(400, "Bad Request"), - UNAUTHORIZED(401, "Unauthorized"), - FORBIDDEN(403, "Forbidden"), - NOT_FOUND(404, "Not Found"), - METHOD_NOT_ALLOWED(405, "Method Not Allowed"), - RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), + private int splitbyte; - INTERNAL_ERROR(500, "Internal Server Error"); + private int rlen; - private final int requestStatus; - private final String description; + private String uri; - Status(int requestStatus, String description) { - this.requestStatus = requestStatus; - this.description = description; - } + private Method method; - @Override - public int getRequestStatus() { - return this.requestStatus; - } + private Map<String, String> parms; - @Override - public String getDescription() { - return "" + this.requestStatus + " " + description; - } - } - } + private Map<String, String> headers; - public static final class ResponseException extends Exception { + private CookieHandler cookies; - private final Response.Status status; + private String queryParameterString; - public ResponseException(Response.Status status, String message) { - super(message); - this.status = status; - } + private String remoteIp; - public ResponseException(Response.Status status, String message, Exception e) { - super(message, e); - this.status = status; - } + private String protocolVersion; - public Response.Status getStatus() { - return status; + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; } - } - /** - * Default strategy for creating and cleaning up temporary files. - */ - private class DefaultTempFileManagerFactory implements TempFileManagerFactory { - @Override - public TempFileManager create() { - return new DefaultTempFileManager(); + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); + this.headers = new HashMap<String, String>(); } - } - /** - * Handles one session, i.e. parses the HTTP request and returns the response. - */ - public interface IHTTPSession { - void execute() throws IOException; + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) throws ResponseException { + try { + // Read the request line + String inLine = in.readLine(); + if (inLine == null) { + return; + } - Map<String, String> getParms(); + StringTokenizer st = new StringTokenizer(inLine); + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } - Map<String, String> getHeaders(); + pre.put("method", st.nextToken()); - /** - * @return the path part of the URL. - */ - String getUri(); + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + } - String getQueryParameterString(); + String uri = st.nextToken(); - Method getMethod(); + // Decode parameters from the URI + int qmi = uri.indexOf('?'); + if (qmi >= 0) { + decodeParms(uri.substring(qmi + 1), parms); + uri = decodePercent(uri.substring(0, qmi)); + } else { + uri = decodePercent(uri); + } - InputStream getInputStream(); + // If there's another token, its protocol version, + // followed by HTTP headers. + // NOTE: this now forces header names lower case since they are + // case insensitive and vary by client. + if (st.hasMoreTokens()) { + protocolVersion = st.nextToken(); + } else { + protocolVersion = "HTTP/1.1"; + NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1."); + } + String line = in.readLine(); + while (line != null && line.trim().length() > 0) { + int p = line.indexOf(':'); + if (p >= 0) { + headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); + } + line = in.readLine(); + } - CookieHandler getCookies(); + pre.put("uri", uri); + } catch (IOException ioe) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } /** - * Adds the files in the request body to the files map. - * @arg files - map to modify + * Decodes the Multipart Body data and put it into Key/Value pairs. */ - void parseBody(Map<String, String> files) throws IOException, ResponseException; - } + private void decodeMultipartFormData(String boundary, String encoding, ByteBuffer fbuf, Map<String, String> parms, Map<String, String> files) throws ResponseException { + try { + int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes()); + if (boundary_idxs.length < 2) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); + } - protected class HTTPSession implements IHTTPSession { - public static final int BUFSIZE = 8192; - private final TempFileManager tempFileManager; - private final OutputStream outputStream; - private PushbackInputStream inputStream; - private int splitbyte; - private int rlen; - private String uri; - private Method method; - private Map<String, String> parms; - private Map<String, String> headers; - private CookieHandler cookies; - private String queryParameterString; + byte[] part_header_buff = new byte[MAX_HEADER_SIZE]; + for (int bi = 0; bi < boundary_idxs.length - 1; bi++) { + fbuf.position(boundary_idxs[bi]); + int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; + fbuf.get(part_header_buff, 0, len); + BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(part_header_buff, 0, len), Charset.forName(encoding)), len); + + int headerLines = 0; + // First line is boundary string + String mpline = in.readLine(); + headerLines++; + if (!mpline.contains(boundary)) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); + } - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - this.tempFileManager = tempFileManager; - this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); - this.outputStream = outputStream; + String part_name = null, file_name = null, content_type = null; + // Parse the reset of the header lines + mpline = in.readLine(); + headerLines++; + while (mpline != null && mpline.trim().length() > 0) { + Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); + if (matcher.matches()) { + String attributeString = matcher.group(2); + matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); + while (matcher.find()) { + String key = matcher.group(1); + if (key.equalsIgnoreCase("name")) { + part_name = matcher.group(2); + } else if (key.equalsIgnoreCase("filename")) { + file_name = matcher.group(2); + } + } + } + matcher = CONTENT_TYPE_PATTERN.matcher(mpline); + if (matcher.matches()) { + content_type = matcher.group(2).trim(); + } + mpline = in.readLine(); + headerLines++; + } + int part_header_len = 0; + while (headerLines-- > 0) { + part_header_len = scipOverNewLine(part_header_buff, part_header_len); + } + // Read the part data + if (part_header_len >= len - 4) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); + } + int part_data_start = boundary_idxs[bi] + part_header_len; + int part_data_end = boundary_idxs[bi + 1] - 4; + + fbuf.position(part_data_start); + if (content_type == null) { + // Read the part into a string + byte[] data_bytes = new byte[part_data_end - part_data_start]; + fbuf.get(data_bytes); + parms.put(part_name, new String(data_bytes, encoding)); + } else { + // Read it into a file + String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start, file_name); + if (!files.containsKey(part_name)) { + files.put(part_name, path); + } else { + int count = 2; + while (files.containsKey(part_name + count)) { + count++; + } + files.put(part_name + count, path); + } + parms.put(part_name, file_name); + } + } + } catch (ResponseException re) { + throw re; + } catch (Exception e) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); + } } - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; - this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); - this.outputStream = outputStream; - String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); - headers = new HashMap<String, String>(); + private int scipOverNewLine(byte[] part_header_buff, int index) { + while (part_header_buff[index] != '\n') { + index++; + } + return ++index; + } - headers.put("remote-addr", remoteIp); - headers.put("http-client-ip", remoteIp); + /** + * Decodes parameters in percent-encoded URI-format ( e.g. + * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given + * Map. NOTE: this doesn't support multiple identical keys due to the + * simplicity of Map. + */ + private void decodeParms(String parms, Map<String, String> p) { + if (parms == null) { + this.queryParameterString = ""; + return; + } + + this.queryParameterString = parms; + StringTokenizer st = new StringTokenizer(parms, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + if (sep >= 0) { + p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1))); + } else { + p.put(decodePercent(e).trim(), ""); + } + } } @Override public void execute() throws IOException { + Response r = null; try { // Read the first 8192 bytes. // The full header should fit in here. // Apache's default header limit is 8KB. - // Do NOT assume that a single read will get the entire header at once! - byte[] buf = new byte[BUFSIZE]; - splitbyte = 0; - rlen = 0; - { - int read = -1; - try { - read = inputStream.read(buf, 0, BUFSIZE); - } catch (Exception e) { - safeClose(inputStream); - safeClose(outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - if (read == -1) { - // socket was been closed - safeClose(inputStream); - safeClose(outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - while (read > 0) { - rlen += read; - splitbyte = findHeaderEnd(buf, rlen); - if (splitbyte > 0) - break; - read = inputStream.read(buf, rlen, BUFSIZE - rlen); + // Do NOT assume that a single read will get the entire header + // at once! + byte[] buf = new byte[HTTPSession.BUFSIZE]; + this.splitbyte = 0; + this.rlen = 0; + + int read = -1; + this.inputStream.mark(HTTPSession.BUFSIZE); + try { + read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); + } catch (Exception e) { + safeClose(this.inputStream); + safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + if (read == -1) { + // socket was been closed + safeClose(this.inputStream); + safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + while (read > 0) { + this.rlen += read; + this.splitbyte = findHeaderEnd(buf, this.rlen); + if (this.splitbyte > 0) { + break; } + read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); } - if (splitbyte < rlen) { - inputStream.unread(buf, splitbyte, rlen - splitbyte); + if (this.splitbyte < this.rlen) { + this.inputStream.reset(); + this.inputStream.skip(this.splitbyte); } - parms = new HashMap<String, String>(); - if(null == headers) { - headers = new HashMap<String, String>(); + this.parms = new HashMap<String, String>(); + if (null == this.headers) { + this.headers = new HashMap<String, String>(); + } else { + this.headers.clear(); } // Create a BufferedReader for parsing the header. - BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); + BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); // Decode the header into parms and header java properties Map<String, String> pre = new HashMap<String, String>(); - decodeHeader(hin, pre, parms, headers); + decodeHeader(hin, pre, this.parms, this.headers); + + if (null != this.remoteIp) { + this.headers.put("remote-addr", this.remoteIp); + this.headers.put("http-client-ip", this.remoteIp); + } - method = Method.lookup(pre.get("method")); - if (method == null) { + this.method = Method.lookup(pre.get("method")); + if (this.method == null) { throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); } - uri = pre.get("uri"); + this.uri = pre.get("uri"); - cookies = new CookieHandler(headers); + this.cookies = new CookieHandler(this.headers); + + String connection = this.headers.get("connection"); + boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*")); // Ok, now do the serve() - Response r = serve(this); + + // TODO: long body_size = getBodySize(); + // TODO: long pos_before_serve = this.inputStream.totalRead() + // (requires implementaion for totalRead()) + r = serve(this); + // TODO: this.inputStream.skip(body_size - + // (this.inputStream.totalRead() - pos_before_serve)) + if (r == null) { throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); } else { - cookies.unloadQueue(r); - r.setRequestMethod(method); - r.send(outputStream); + String acceptEncoding = this.headers.get("accept-encoding"); + this.cookies.unloadQueue(r); + r.setRequestMethod(this.method); + r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); + r.setKeepAlive(keepAlive); + r.send(this.outputStream); + } + if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) { + throw new SocketException("NanoHttpd Shutdown"); } } catch (SocketException e) { // throw it out to close socket object (finalAccept) throw e; } catch (SocketTimeoutException ste) { - throw ste; + // treat socket timeouts the same way we treat socket exceptions + // i.e. close the stream & finalAccept object by throwing the + // exception up the call stack. + throw ste; } catch (IOException ioe) { - Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - r.send(outputStream); - safeClose(outputStream); + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); } catch (ResponseException re) { - Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); - r.send(outputStream); - safeClose(outputStream); + Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); } finally { - tempFileManager.clear(); + safeClose(r); + this.tempFileManager.clear(); + } + } + + /** + * Find byte index separating header from body. It must be the last byte + * of the first two sequential new lines. + */ + private int findHeaderEnd(final byte[] buf, int rlen) { + int splitbyte = 0; + while (splitbyte + 1 < rlen) { + + // RFC2616 + if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { + return splitbyte + 4; + } + + // tolerance + if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') { + return splitbyte + 2; + } + splitbyte++; + } + return 0; + } + + /** + * Find the byte positions where multipart boundaries start. This reads + * a large block at a time and uses a temporary buffer to optimize + * (memory mapped) file access. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int[] res = new int[0]; + if (b.remaining() < boundary.length) { + return res; } + + int search_window_pos = 0; + byte[] search_window = new byte[4 * 1024 + boundary.length]; + + int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; + b.get(search_window, 0, first_fill); + int new_bytes = first_fill - boundary.length; + + do { + // Search the search_window + for (int j = 0; j < new_bytes; j++) { + for (int i = 0; i < boundary.length; i++) { + if (search_window[j + i] != boundary[i]) + break; + if (i == boundary.length - 1) { + // Match found, add it to results + int[] new_res = new int[res.length + 1]; + System.arraycopy(res, 0, new_res, 0, res.length); + new_res[res.length] = search_window_pos + j; + res = new_res; + } + } + } + search_window_pos += new_bytes; + + // Copy the end of the buffer to the start + System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); + + // Refill search_window + new_bytes = search_window.length - boundary.length; + new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; + b.get(search_window, boundary.length, new_bytes); + } while (new_bytes > 0); + return res; } @Override - public void parseBody(Map<String, String> files) throws IOException, ResponseException { - RandomAccessFile randomAccessFile = null; - BufferedReader in = null; + public CookieHandler getCookies() { + return this.cookies; + } + + @Override + public final Map<String, String> getHeaders() { + return this.headers; + } + + @Override + public final InputStream getInputStream() { + return this.inputStream; + } + + @Override + public final Method getMethod() { + return this.method; + } + + @Override + public final Map<String, String> getParms() { + return this.parms; + } + + @Override + public String getQueryParameterString() { + return this.queryParameterString; + } + + private RandomAccessFile getTmpBucket() { try { + TempFile tempFile = this.tempFileManager.createTempFile(null); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } - randomAccessFile = getTmpBucket(); + @Override + public final String getUri() { + return this.uri; + } - long size; - if (headers.containsKey("content-length")) { - size = Integer.parseInt(headers.get("content-length")); - } else if (splitbyte < rlen) { - size = rlen - splitbyte; + /** + * Deduce body length in bytes. Either from "content-length" header or + * read bytes. + */ + public long getBodySize() { + if (this.headers.containsKey("content-length")) { + return Long.parseLong(this.headers.get("content-length")); + } else if (this.splitbyte < this.rlen) { + return this.rlen - this.splitbyte; + } + return 0; + } + + @Override + public void parseBody(Map<String, String> files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + try { + long size = getBodySize(); + ByteArrayOutputStream baos = null; + DataOutput request_data_output = null; + + // Store the request in memory or a file, depending on size + if (size < MEMORY_STORE_LIMIT) { + baos = new ByteArrayOutputStream(); + request_data_output = new DataOutputStream(baos); } else { - size = 0; + randomAccessFile = getTmpBucket(); + request_data_output = randomAccessFile; } - // Now read all the body and write it to f - byte[] buf = new byte[512]; - while (rlen >= 0 && size > 0) { - rlen = inputStream.read(buf, 0, (int)Math.min(size, 512)); - size -= rlen; - if (rlen > 0) { - randomAccessFile.write(buf, 0, rlen); + // Read all the body and write it to request_data_output + byte[] buf = new byte[REQUEST_BUFFER_LEN]; + while (this.rlen >= 0 && size > 0) { + this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); + size -= this.rlen; + if (this.rlen > 0) { + request_data_output.write(buf, 0, this.rlen); } } - // Get the raw body as a byte [] - ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); - randomAccessFile.seek(0); - - // Create a BufferedReader for easily reading it as string. - InputStream bin = new FileInputStream(randomAccessFile.getFD()); - in = new BufferedReader(new InputStreamReader(bin)); + ByteBuffer fbuf = null; + if (baos != null) { + fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); + } else { + fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); + randomAccessFile.seek(0); + } // If the method is POST, there may be parameters // in data section, too, read it: - if (Method.POST.equals(method)) { + if (Method.POST.equals(this.method)) { String contentType = ""; - String contentTypeHeader = headers.get("content-type"); + String contentTypeHeader = this.headers.get("content-type"); StringTokenizer st = null; if (contentTypeHeader != null) { @@ -1021,426 +1053,1093 @@ public abstract class NanoHTTPD { if ("multipart/form-data".equalsIgnoreCase(contentType)) { // Handle multipart/form-data if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); } - - String boundaryStartString = "boundary="; - int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); - String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); - if (boundary.startsWith("\"") && boundary.endsWith("\"")) { - boundary = boundary.substring(1, boundary.length() - 1); - } - - decodeMultipartData(boundary, fbuf, in, parms, files); + decodeMultipartFormData(getAttributeFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null), // + getAttributeFromContentHeader(contentTypeHeader, CHARSET_PATTERN, "US-ASCII"), fbuf, this.parms, files); } else { - String postLine = ""; - StringBuilder postLineBuffer = new StringBuilder(); - char pbuf[] = new char[512]; - int read = in.read(pbuf); - while (read >= 0 && !postLine.endsWith("\r\n")) { - postLine = String.valueOf(pbuf, 0, read); - postLineBuffer.append(postLine); - read = in.read(pbuf); - } - postLine = postLineBuffer.toString().trim(); + byte[] postBytes = new byte[fbuf.remaining()]; + fbuf.get(postBytes); + String postLine = new String(postBytes).trim(); // Handle application/x-www-form-urlencoded if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { - decodeParms(postLine, parms); + decodeParms(postLine, this.parms); } else if (postLine.length() != 0) { - // Special case for raw POST data => create a special files entry "postData" with raw content data - files.put("postData", postLine); + // Special case for raw POST data => create a + // special files entry "postData" with raw content + // data + files.put("postData", postLine); } } - } else if (Method.PUT.equals(method)) { - files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); + } else if (Method.PUT.equals(this.method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); } } finally { safeClose(randomAccessFile); - safeClose(in); } } + private String getAttributeFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue) { + Matcher matcher = pattern.matcher(contentTypeHeader); + return matcher.find() ? matcher.group(2) : defaultValue; + } + /** - * Decodes the sent headers and loads the data into Key/value pairs + * Retrieves the content of a sent file and saves it to a temporary + * file. The full path to the saved file is returned. */ - private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) - throws ResponseException { - try { - // Read the request line - String inLine = in.readLine(); - if (inLine == null) { - return; + private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + safeClose(fileOutputStream); } + } + return path; + } + } - StringTokenizer st = new StringTokenizer(inLine); - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); - } + /** + * Handles one session, i.e. parses the HTTP request and returns the + * response. + */ + public interface IHTTPSession { - pre.put("method", st.nextToken()); + void execute() throws IOException; - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); - } + CookieHandler getCookies(); - String uri = st.nextToken(); + Map<String, String> getHeaders(); - // Decode parameters from the URI - int qmi = uri.indexOf('?'); - if (qmi >= 0) { - decodeParms(uri.substring(qmi + 1), parms); - uri = decodePercent(uri.substring(0, qmi)); - } else { - uri = decodePercent(uri); - } + InputStream getInputStream(); - // If there's another token, it's protocol version, - // followed by HTTP headers. Ignore version but parse headers. - // NOTE: this now forces header names lowercase since they are - // case insensitive and vary by client. - if (st.hasMoreTokens()) { - String line = in.readLine(); - while (line != null && line.trim().length() > 0) { - int p = line.indexOf(':'); - if (p >= 0) - headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); - line = in.readLine(); - } - } + Method getMethod(); - pre.put("uri", uri); - } catch (IOException ioe) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + Map<String, String> getParms(); + + String getQueryParameterString(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + /** + * Adds the files in the request body to the files map. + * + * @param files + * map to modify + */ + void parseBody(Map<String, String> files) throws IOException, ResponseException; + } + + /** + * HTTP Request methods, with the ability to decode a <code>String</code> + * back to its enum value. + */ + public enum Method { + GET, + PUT, + POST, + DELETE, + HEAD, + OPTIONS, + TRACE, + CONNECT, + PATCH; + + static Method lookup(String method) { + for (Method m : Method.values()) { + if (m.toString().equalsIgnoreCase(method)) { + return m; + } } + return null; + } + } + + /** + * HTTP response. Return one of these from serve(). + */ + public static class Response implements Closeable { + + public interface IStatus { + + String getDescription(); + + int getRequestStatus(); } /** - * Decodes the Multipart Body data and put it into Key/Value pairs. + * Some HTTP response status codes */ - private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, - Map<String, String> files) throws ResponseException { - try { - int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); - int boundarycount = 1; - String mpline = in.readLine(); - while (mpline != null) { - if (!mpline.contains(boundary)) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); - } - boundarycount++; - Map<String, String> item = new HashMap<String, String>(); - mpline = in.readLine(); - while (mpline != null && mpline.trim().length() > 0) { - int p = mpline.indexOf(':'); - if (p != -1) { - item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); - } - mpline = in.readLine(); - } - if (mpline != null) { - String contentDisposition = item.get("content-disposition"); - if (contentDisposition == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); - } - StringTokenizer st = new StringTokenizer(contentDisposition, ";"); - Map<String, String> disposition = new HashMap<String, String>(); - while (st.hasMoreTokens()) { - String token = st.nextToken().trim(); - int p = token.indexOf('='); - if (p != -1) { - disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); - } - } - String pname = disposition.get("name"); - pname = pname.substring(1, pname.length() - 1); - - String value = ""; - if (item.get("content-type") == null) { - while (mpline != null && !mpline.contains(boundary)) { - mpline = in.readLine(); - if (mpline != null) { - int d = mpline.indexOf(boundary); - if (d == -1) { - value += mpline; - } else { - value += mpline.substring(0, d - 2); - } - } - } - } else { - if (boundarycount > bpositions.length) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); - } - int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); - String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); - files.put(pname, path); - value = disposition.get("filename"); - value = value.substring(1, value.length() - 1); - do { - mpline = in.readLine(); - } while (mpline != null && !mpline.contains(boundary)); - } - parms.put(pname, value); - } - } - } catch (IOException ioe) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + public enum Status implements IStatus { + SWITCH_PROTOCOL(101, "Switching Protocols"), + OK(200, "OK"), + CREATED(201, "Created"), + ACCEPTED(202, "Accepted"), + NO_CONTENT(204, "No Content"), + PARTIAL_CONTENT(206, "Partial Content"), + REDIRECT(301, "Moved Permanently"), + TEMPORARY_REDIRECT(302, "Moved Temporarily"), + NOT_MODIFIED(304, "Not Modified"), + BAD_REQUEST(400, "Bad Request"), + UNAUTHORIZED(401, "Unauthorized"), + FORBIDDEN(403, "Forbidden"), + NOT_FOUND(404, "Not Found"), + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + NOT_ACCEPTABLE(406, "Not Acceptable"), + REQUEST_TIMEOUT(408, "Request Timeout"), + CONFLICT(409, "Conflict"), + RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), + INTERNAL_ERROR(500, "Internal Server Error"), + NOT_IMPLEMENTED(501, "Not Implemented"), + UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); + + private final int requestStatus; + + private final String description; + + Status(int requestStatus, String description) { + this.requestStatus = requestStatus; + this.description = description; } + + @Override + public String getDescription() { + return "" + this.requestStatus + " " + this.description; + } + + @Override + public int getRequestStatus() { + return this.requestStatus; + } + } /** - * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. + * Output stream that will automatically send every write to the wrapped + * OutputStream according to chunked transfer: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 */ - private int findHeaderEnd(final byte[] buf, int rlen) { - int splitbyte = 0; - while (splitbyte + 3 < rlen) { - if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { - return splitbyte + 4; - } - splitbyte++; + private static class ChunkedOutputStream extends FilterOutputStream { + + public ChunkedOutputStream(OutputStream out) { + super(out); } - return 0; + + @Override + public void write(int b) throws IOException { + byte[] data = { + (byte) b + }; + write(data, 0, 1); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len == 0) + return; + out.write(String.format("%x\r\n", len).getBytes()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } + } /** - * Find the byte positions where multipart boundaries start. + * HTTP status code after processing, e.g. "200 OK", Status.OK */ - private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { - int matchcount = 0; - int matchbyte = -1; - List<Integer> matchbytes = new ArrayList<Integer>(); - for (int i = 0; i < b.limit(); i++) { - if (b.get(i) == boundary[matchcount]) { - if (matchcount == 0) - matchbyte = i; - matchcount++; - if (matchcount == boundary.length) { - matchbytes.add(matchbyte); - matchcount = 0; - matchbyte = -1; - } - } else { - i -= matchcount; - matchcount = 0; - matchbyte = -1; - } + private IStatus status; + + /** + * MIME type of content, e.g. "text/html" + */ + private String mimeType; + + /** + * Data of the response, may be null. + */ + private InputStream data; + + private long contentLength; + + /** + * Headers for the HTTP response. Use addHeader() to add lines. + */ + private final Map<String, String> header = new HashMap<String, String>(); + + /** + * The request method that spawned this response. + */ + private Method requestMethod; + + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + private boolean encodeAsGzip; + + private boolean keepAlive; + + /** + * Creates a fixed length response if totalBytes>=0, otherwise chunked. + */ + protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { + this.status = status; + this.mimeType = mimeType; + if (data == null) { + this.data = new ByteArrayInputStream(new byte[0]); + this.contentLength = 0L; + } else { + this.data = data; + this.contentLength = totalBytes; } - int[] ret = new int[matchbytes.size()]; - for (int i = 0; i < ret.length; i++) { - ret[i] = matchbytes.get(i); + this.chunkedTransfer = this.contentLength < 0; + keepAlive = true; + } + + @Override + public void close() throws IOException { + if (this.data != null) { + this.data.close(); } - return ret; } /** - * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. + * Adds given line to the header. */ - private String saveTmpFile(ByteBuffer b, int offset, int len) { - String path = ""; - if (len > 0) { - FileOutputStream fileOutputStream = null; - try { - TempFile tempFile = tempFileManager.createTempFile(); - ByteBuffer src = b.duplicate(); - fileOutputStream = new FileOutputStream(tempFile.getName()); - FileChannel dest = fileOutputStream.getChannel(); - src.position(offset).limit(offset + len); - dest.write(src.slice()); - path = tempFile.getName(); - } catch (Exception e) { // Catch exception if any - throw new Error(e); // we won't recover, so throw an error - } finally { - safeClose(fileOutputStream); + public void addHeader(String name, String value) { + this.header.put(name, value); + } + + public InputStream getData() { + return this.data; + } + + public String getHeader(String name) { + for (String headerName : header.keySet()) { + if (headerName.equalsIgnoreCase(name)) { + return header.get(headerName); } } - return path; + return null; } - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = tempFileManager.createTempFile(); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - throw new Error(e); // we won't recover, so throw an error + public String getMimeType() { + return this.mimeType; + } + + public Method getRequestMethod() { + return this.requestMethod; + } + + public IStatus getStatus() { + return this.status; + } + + public void setGzipEncoding(boolean encodeAsGzip) { + this.encodeAsGzip = encodeAsGzip; + } + + public void setKeepAlive(boolean useKeepAlive) { + this.keepAlive = useKeepAlive; + } + + private static boolean headerAlreadySent(Map<String, String> header, String name) { + boolean alreadySent = false; + for (String headerName : header.keySet()) { + alreadySent |= headerName.equalsIgnoreCase(name); } + return alreadySent; } /** - * It returns the offset separating multipart file headers from the file's data. + * Sends given response to the socket. */ - private int stripMultipartHeaders(ByteBuffer b, int offset) { - int i; - for (i = offset; i < b.limit(); i++) { - if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { - break; + protected void send(OutputStream outputStream) { + String mime = this.mimeType; + SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + + try { + if (this.status == null) { + throw new Error("sendResponse(): Status can't be null."); } + PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); + pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); + + if (mime != null) { + pw.print("Content-Type: " + mime + "\r\n"); + } + + if (this.header == null || this.header.get("Date") == null) { + pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); + } + + if (this.header != null) { + for (String key : this.header.keySet()) { + String value = this.header.get(key); + pw.print(key + ": " + value + "\r\n"); + } + } + + if (!headerAlreadySent(header, "connection")) { + pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n"); + } + + if (headerAlreadySent(this.header, "content-length")) { + encodeAsGzip = false; + } + + if (encodeAsGzip) { + pw.print("Content-Encoding: gzip\r\n"); + setChunkedTransfer(true); + } + + long pending = this.data != null ? this.contentLength : 0; + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + pw.print("Transfer-Encoding: chunked\r\n"); + } else if (!encodeAsGzip) { + pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); + } + pw.print("\r\n"); + pw.flush(); + sendBodyWithCorrectTransferAndEncoding(outputStream, pending); + outputStream.flush(); + safeClose(this.data); + } catch (IOException ioe) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); + } + } + + private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); + sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + chunkedOutputStream.finish(); + } else { + sendBodyWithCorrectEncoding(outputStream, pending); + } + } + + private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { + if (encodeAsGzip) { + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + sendBody(gzipOutputStream, -1); + gzipOutputStream.finish(); + } else { + sendBody(outputStream, pending); } - return i + 1; } /** - * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and - * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map. + * Sends the body to the specified OutputStream. The pending parameter + * limits the maximum amounts of bytes sent unless it is -1, in which + * case everything is sent. + * + * @param outputStream + * the OutputStream to send data to + * @param pending + * -1 to send everything, otherwise sets a max limit to the + * number of bytes sent + * @throws IOException + * if something goes wrong while sending the data. */ - private void decodeParms(String parms, Map<String, String> p) { - if (parms == null) { - queryParameterString = ""; - return; + private void sendBody(OutputStream outputStream, long pending) throws IOException { + long BUFFER_SIZE = 16 * 1024; + byte[] buff = new byte[(int) BUFFER_SIZE]; + boolean sendEverything = pending == -1; + while (pending > 0 || sendEverything) { + long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); + int read = this.data.read(buff, 0, (int) bytesToRead); + if (read <= 0) { + break; + } + outputStream.write(buff, 0, read); + if (!sendEverything) { + pending -= read; + } } + } - queryParameterString = parms; - StringTokenizer st = new StringTokenizer(parms, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - if (sep >= 0) { - p.put(decodePercent(e.substring(0, sep)).trim(), - decodePercent(e.substring(sep + 1))); - } else { - p.put(decodePercent(e).trim(), ""); + protected static long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) { + for (String headerName : header.keySet()) { + if (headerName.equalsIgnoreCase("content-length")) { + try { + return Long.parseLong(header.get(headerName)); + } catch (NumberFormatException ex) { + return size; + } } } + + pw.print("Content-Length: " + size + "\r\n"); + return size; } - @Override - public final Map<String, String> getParms() { - return parms; + public void setChunkedTransfer(boolean chunkedTransfer) { + this.chunkedTransfer = chunkedTransfer; } - public String getQueryParameterString() { - return queryParameterString; + public void setData(InputStream data) { + this.data = data; } - @Override - public final Map<String, String> getHeaders() { - return headers; + public void setMimeType(String mimeType) { + this.mimeType = mimeType; } - @Override - public final String getUri() { - return uri; + public void setRequestMethod(Method requestMethod) { + this.requestMethod = requestMethod; } - @Override - public final Method getMethod() { - return method; + public void setStatus(IStatus status) { + this.status = status; } + } - @Override - public final InputStream getInputStream() { - return inputStream; + public static final class ResponseException extends Exception { + + private static final long serialVersionUID = 6569838532917408380L; + + private final Response.Status status; + + public ResponseException(Response.Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Response.Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Response.Status getStatus() { + return this.status; + } + } + + /** + * The runnable that will be used for the main listening thread. + */ + public class ServerRunnable implements Runnable { + + private final int timeout; + + private IOException bindException; + + private boolean hasBinded = false; + + private ServerRunnable(int timeout) { + this.timeout = timeout; } @Override - public CookieHandler getCookies() { - return cookies; + public void run() { + try { + myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); + hasBinded = true; + } catch (IOException e) { + this.bindException = e; + return; + } + do { + try { + final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); + if (this.timeout > 0) { + finalAccept.setSoTimeout(this.timeout); + } + final InputStream inputStream = finalAccept.getInputStream(); + NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); + } + } while (!NanoHTTPD.this.myServerSocket.isClosed()); } } - public static class Cookie { - private String n, v, e; + /** + * A temp file. + * <p/> + * <p> + * Temp files are responsible for managing the actual temporary storage and + * cleaning themselves up when no longer needed. + * </p> + */ + public interface TempFile { - public Cookie(String name, String value, String expires) { - n = name; - v = value; - e = expires; + public void delete() throws Exception; + + public String getName(); + + public OutputStream open() throws Exception; + } + + /** + * Temp file manager. + * <p/> + * <p> + * Temp file managers are created 1-to-1 with incoming requests, to create + * and cleanup temporary files created as a result of handling the request. + * </p> + */ + public interface TempFileManager { + + void clear(); + + public TempFile createTempFile(String filename_hint) throws Exception; + } + + /** + * Factory to create temp file managers. + */ + public interface TempFileManagerFactory { + + public TempFileManager create(); + } + + /** + * Factory to create ServerSocketFactories. + */ + public interface ServerSocketFactory { + + public ServerSocket create() throws IOException; + + } + + /** + * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) + * This is required as the Keep-Alive HTTP connections would otherwise block + * the socket reading thread forever (or as long the browser is open). + */ + public static final int SOCKET_READ_TIMEOUT = 5000; + + /** + * Common MIME type for dynamic content: plain text + */ + public static final String MIME_PLAINTEXT = "text/plain"; + + /** + * Common MIME type for dynamic content: html + */ + public static final String MIME_HTML = "text/html"; + + /** + * Pseudo-Parameter to use to store the actual query string in the + * parameters map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + protected static Map<String, String> MIME_TYPES; + + public static Map<String, String> mimeTypes() { + if (MIME_TYPES == null) { + MIME_TYPES = new HashMap<String, String>(); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); + if (MIME_TYPES.isEmpty()) { + LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); + } } + return MIME_TYPES; + } - public Cookie(String name, String value) { - this(name, value, 30); + private static void loadMimeTypes(Map<String, String> result, String resourceName) { + try { + Enumeration<URL> resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); + while (resources.hasMoreElements()) { + URL url = (URL) resources.nextElement(); + Properties properties = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + properties.load(url.openStream()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); + } finally { + safeClose(stream); + } + result.putAll((Map) properties); + } + } catch (IOException e) { + LOG.log(Level.INFO, "no mime types available at " + resourceName); } + }; - public Cookie(String name, String value, int numDays) { - n = name; - v = value; - e = getHTTPTime(numDays); + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an + * array of loaded KeyManagers. These objects must properly + * loaded/initialized by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { + SSLServerSocketFactory res = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadedKeyStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + res = ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); } + return res; + } - public String getHTTPHeader() { - String fmt = "%s=%s; expires=%s"; - return String.format(fmt, n, v, e); + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a + * loaded KeyManagerFactory. These objects must properly loaded/initialized + * by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { + try { + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); + } catch (Exception e) { + throw new IOException(e.getMessage()); } + } - public static String getHTTPTime(int days) { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - calendar.add(Calendar.DAY_OF_MONTH, days); - return dateFormat.format(calendar.getTime()); + /** + * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your + * certificate and passphrase + */ + public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + return makeSSLSocketFactory(keystore, keyManagerFactory); + } catch (Exception e) { + throw new IOException(e.getMessage()); } } /** - * Provides rudimentary support for cookies. - * Doesn't support 'path', 'secure' nor 'httpOnly'. - * Feel free to improve it and/or add unsupported features. - * - * @author LordFokas + * Get MIME type from file name extension, if possible + * + * @param uri + * the string representing a file + * @return the connected mime/type */ - public class CookieHandler implements Iterable<String> { - private HashMap<String, String> cookies = new HashMap<String, String>(); - private ArrayList<Cookie> queue = new ArrayList<Cookie>(); + public static String getMimeTypeForFile(String uri) { + int dot = uri.lastIndexOf('.'); + String mime = null; + if (dot >= 0) { + mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); + } + return mime == null ? "application/octet-stream" : mime; + } - public CookieHandler(Map<String, String> httpHeaders) { - String raw = httpHeaders.get("cookie"); - if (raw != null) { - String[] tokens = raw.split(";"); - for (String token : tokens) { - String[] data = token.trim().split("="); - if (data.length == 2) { - cookies.put(data[0], data[1]); - } + private static final void safeClose(Object closeable) { + try { + if (closeable != null) { + if (closeable instanceof Closeable) { + ((Closeable) closeable).close(); + } else if (closeable instanceof Socket) { + ((Socket) closeable).close(); + } else if (closeable instanceof ServerSocket) { + ((ServerSocket) closeable).close(); + } else { + throw new IllegalArgumentException("Unknown object to close"); } } + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); } + } + + private final String hostname; + + private final int myPort; + + private volatile ServerSocket myServerSocket; + + private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory(); + + private Thread myThread; + + /** + * Pluggable strategy for asynchronously executing requests. + */ + protected AsyncRunner asyncRunner; + + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private TempFileManagerFactory tempFileManagerFactory; + + /** + * Constructs an HTTP server on given port. + */ + public NanoHTTPD(int port) { + this(null, port); + } + + // ------------------------------------------------------------------------------- + // // + // + // Threading Strategy. + // + // ------------------------------------------------------------------------------- + // // + + /** + * Constructs an HTTP server on given hostname and port. + */ + public NanoHTTPD(String hostname, int port) { + this.hostname = hostname; + this.myPort = port; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + setAsyncRunner(new DefaultAsyncRunner()); + } + + /** + * Forcibly closes all connections that are open. + */ + public synchronized void closeAllConnections() { + stop(); + } + + /** + * create a instance of the client handler, subclasses can return a subclass + * of the ClientHandler. + * + * @param finalAccept + * the socket the cleint is connected to + * @param inputStream + * the input stream + * @return the client handler + */ + protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { + return new ClientHandler(inputStream, finalAccept); + } + + /** + * Instantiate the server runnable, can be overwritten by subclasses to + * provide a subclass of the ServerRunnable. + * + * @param timeout + * the socet timeout to use. + * @return the server runnable. + */ + protected ServerRunnable createServerRunnable(final int timeout) { + return new ServerRunnable(timeout); + } - @Override public Iterator<String> iterator() { - return cookies.keySet().iterator(); + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param parms + * original <b>NanoHTTPD</b> parameters values, as passed to the + * <code>serve()</code> method. + * @return a map of <code>String</code> (parameter name) to + * <code>List<String></code> (a list of the values supplied). + */ + protected static Map<String, List<String>> decodeParameters(Map<String, String> parms) { + return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); + } + + // ------------------------------------------------------------------------------- + // // + + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param queryString + * a query string pulled from the URL. + * @return a map of <code>String</code> (parameter name) to + * <code>List<String></code> (a list of the values supplied). + */ + protected static Map<String, List<String>> decodeParameters(String queryString) { + Map<String, List<String>> parms = new HashMap<String, List<String>>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList<String>()); + } + String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } } + return parms; + } - /** - * Read a cookie from the HTTP Headers. - * - * @param name The cookie's name. - * @return The cookie's value if it exists, null otherwise. - */ - public String read(String name) { - return cookies.get(name); + /** + * Decode percent encoded <code>String</code> values. + * + * @param str + * the percent encoded <code>String</code> + * @return expanded form of the input, for example "foo%20bar" becomes + * "foo bar" + */ + protected static String decodePercent(String str) { + String decoded = null; + try { + decoded = URLDecoder.decode(str, "UTF8"); + } catch (UnsupportedEncodingException ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); } + return decoded; + } - /** - * Sets a cookie. - * - * @param name The cookie's name. - * @param value The cookie's value. - * @param expires How many days until the cookie expires. - */ - public void set(String name, String value, int expires) { - queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + /** + * @return true if the gzip compression should be used if the client + * accespts it. Default this option is on for text content and off + * for everything. Override this for custom semantics. + */ + protected boolean useGzipWhenAccepted(Response r) { + return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/"); + } + + public final int getListeningPort() { + return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); + } + + public final boolean isAlive() { + return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); + } + + public ServerSocketFactory getServerSocketFactory() { + return serverSocketFactory; + } + + public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + public String getHostname() { + return hostname; + } + + public TempFileManagerFactory getTempFileManagerFactory() { + return tempFileManagerFactory; + } + + /** + * Call before start() to serve over HTTPS instead of HTTP + */ + public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); + } + + /** + * Create a response with unknown length (using HTTP 1.1 chunking). + */ + public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { + return new Response(status, mimeType, data, -1); + } + + /** + * Create a response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { + return new Response(status, mimeType, data, totalBytes); + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { + if (txt == null) { + return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); + } else { + byte[] bytes; + try { + bytes = txt.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); + bytes = new byte[0]; + } + return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); } + } - public void set(Cookie cookie) { - queue.add(cookie); + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(String msg) { + return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); + } + + /** + * Override this to customize the server. + * <p/> + * <p/> + * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param session + * The HTTP session + * @return HTTP response, see class Response for details + */ + public Response serve(IHTTPSession session) { + Map<String, String> files = new HashMap<String, String>(); + Method method = session.getMethod(); + if (Method.PUT.equals(method) || Method.POST.equals(method)) { + try { + session.parseBody(files); + } catch (IOException ioe) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (ResponseException re) { + return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + } } - /** - * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side. - * - * @param name The cookie name. - */ - public void delete(String name) { - set(name, "-delete-", -30); + Map<String, String> parms = session.getParms(); + parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); + return serve(session.getUri(), method, session.getHeaders(), parms, files); + } + + /** + * Override this to customize the server. + * <p/> + * <p/> + * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param uri + * Percent-decoded URI without parameters, for example + * "/index.cgi" + * @param method + * "GET", "POST" etc. + * @param parms + * Parsed, percent decoded parameters from URI and, in case of + * POST, data. + * @param headers + * Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + @Deprecated + public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); + } + + /** + * Pluggable strategy for asynchronously executing requests. + * + * @param asyncRunner + * new strategy for handling threads. + */ + public void setAsyncRunner(AsyncRunner asyncRunner) { + this.asyncRunner = asyncRunner; + } + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory + * new strategy for handling temp files. + */ + public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * Start the server. + * + * @throws IOException + * if the socket is in use. + */ + public void start() throws IOException { + start(NanoHTTPD.SOCKET_READ_TIMEOUT); + } + + /** + * Starts the server (in setDaemon(true) mode). + */ + public void start(final int timeout) throws IOException { + start(timeout, true); + } + + /** + * Start the server. + * + * @param timeout + * timeout to use for socket connections. + * @param daemon + * start the thread daemon or not. + * @throws IOException + * if the socket is in use. + */ + public void start(final int timeout, boolean daemon) throws IOException { + this.myServerSocket = this.getServerSocketFactory().create(); + this.myServerSocket.setReuseAddress(true); + + ServerRunnable serverRunnable = createServerRunnable(timeout); + this.myThread = new Thread(serverRunnable); + this.myThread.setDaemon(daemon); + this.myThread.setName("NanoHttpd Main Listener"); + this.myThread.start(); + while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { + try { + Thread.sleep(10L); + } catch (Throwable e) { + // on android this may not be allowed, that's why we + // catch throwable the wait should be very short because we are + // just waiting for the bind of the socket + } } + if (serverRunnable.bindException != null) { + throw serverRunnable.bindException; + } + } - /** - * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers. - * - * @param response The Response object to which headers the queued cookies will be added. - */ - public void unloadQueue(Response response) { - for (Cookie cookie : queue) { - response.addHeader("Set-Cookie", cookie.getHTTPHeader()); + /** + * Stop the server. + */ + public void stop() { + try { + safeClose(this.myServerSocket); + this.asyncRunner.closeAll(); + if (this.myThread != null) { + this.myThread.join(); } + } catch (Exception e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); } } + + public final boolean wasStarted() { + return this.myServerSocket != null && this.myThread != null; + } } diff --git a/core/src/main/java/fi/iki/elonen/util/ServerRunner.java b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java new file mode 100644 index 0000000..e0aa3db --- /dev/null +++ b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java @@ -0,0 +1,75 @@ +package fi.iki.elonen.util; + +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import fi.iki.elonen.NanoHTTPD; + +public class ServerRunner { + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(ServerRunner.class.getName()); + + public static void executeInstance(NanoHTTPD server) { + try { + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); + } catch (IOException ioe) { + System.err.println("Couldn't start server:\n" + ioe); + System.exit(-1); + } + + System.out.println("Server started, Hit Enter to stop.\n"); + + try { + System.in.read(); + } catch (Throwable ignored) { + } + + server.stop(); + System.out.println("Server stopped.\n"); + } + + public static <T extends NanoHTTPD> void run(Class<T> serverClass) { + try { + executeInstance(serverClass.newInstance()); + } catch (Exception e) { + ServerRunner.LOG.log(Level.SEVERE, "Cound nor create server", e); + } + } +} diff --git a/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties b/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties new file mode 100644 index 0000000..3fb242f --- /dev/null +++ b/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties @@ -0,0 +1,30 @@ +#default mime types for nanohttpd, use META-INF/mimetypes.properties for user defined mimetypes +css=text/css +htm=text/html +html=text/html +xml=text/xml +java=text/x-java-source, text/java +md=text/plain +txt=text/plain +asc=text/plain +gif=image/gif +jpg=image/jpeg +jpeg=image/jpeg +png=image/png +svg=image/svg+xml +mp3=audio/mpeg +m3u=audio/mpeg-url +mp4=video/mp4 +ogv=video/ogg +flv=video/x-flv +mov=video/quicktime +swf=application/x-shockwave-flash +js=application/javascript +pdf=application/pdf +doc=application/msword +ogg=application/x-ogg +zip=application/octet-stream +exe=application/octet-stream +class=application/octet-stream +m3u8=application/vnd.apple.mpegurl +ts=video/mp2t
\ No newline at end of file diff --git a/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties b/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties new file mode 100644 index 0000000..7166a88 --- /dev/null +++ b/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties @@ -0,0 +1 @@ +#mime types for nanohttpd, use a file like this for user defined mimetypes
\ No newline at end of file diff --git a/core/src/site/site.xml b/core/src/site/site.xml new file mode 100644 index 0000000..4270945 --- /dev/null +++ b/core/src/site/site.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<project name="${project.name}"> + <skin> + <groupId>org.apache.maven.skins</groupId> + <artifactId>maven-fluido-skin</artifactId> + <version>1.3.0</version> + </skin> + <bannerLeft> + <src>../images/nanohttpd_logo.png</src> + </bannerLeft> + <bannerRight> + <src>../images/nanohttpd_logo_text.png</src> + </bannerRight> + <publishDate position="left" format="yyyy-MM-dd" /> + <version position="right" /> + <poweredBy> + <logo name="Maven" href="http://maven.apache.org/" + img="http://maven.apache.org/images/logos/maven-feather.png" /> + </poweredBy> + <custom> + <fluidoSkin> + <topBarEnabled>false</topBarEnabled> + <sideBarEnabled>true</sideBarEnabled> + <gitHub> + <projectId>Nanohttpd/nanohttpd</projectId> + <ribbonOrientation>right</ribbonOrientation> + <ribbonColor>black</ribbonColor> + </gitHub> + </fluidoSkin> + </custom> + <body> + <breadcrumbs> + <item name="${project.name}" href="index.html" /> + </breadcrumbs> + <menu name="Documentation"> + <item name="About" href="index.html" /> + </menu> + <menu ref="modules" /> + <menu ref="reports" /> + </body> +</project>
\ No newline at end of file diff --git a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java index c3fb1f0..8853efa 100644 --- a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java @@ -1,46 +1,50 @@ package fi.iki.elonen; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static fi.iki.elonen.NanoHTTPD.Response.Status.OK; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PipedInputStream; -import static fi.iki.elonen.NanoHTTPD.Response.Status.OK; - public class HttpChunkedResponseTest extends HttpServerTest { - @org.junit.Test - public void thatChunkedContentIsChunked() throws Exception { - PipedInputStream pipedInputStream = new ChunkedInputStream(new String[]{ - "some", - "thing which is longer than sixteen characters", - "whee!", - "" - }); - String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: what/ever", - "Date: .*", - "Connection: keep-alive", - "Transfer-Encoding: chunked", - "", - "4", - "some", - "2d", - "thing which is longer than sixteen characters", - "5", - "whee!", - "0", - "" - }; - testServer.response = new NanoHTTPD.Response(OK, "what/ever", pipedInputStream); - testServer.response.setChunkedTransfer(true); - - ByteArrayOutputStream byteArrayOutputStream = invokeServer("GET / HTTP/1.0"); - - assertResponse(byteArrayOutputStream, expected); - } private static class ChunkedInputStream extends PipedInputStream { + int chunk = 0; + String[] chunks; private ChunkedInputStream(String[] chunks) { @@ -48,12 +52,45 @@ public class HttpChunkedResponseTest extends HttpServerTest { } @Override - public synchronized int read(byte[] buffer) throws IOException { + public synchronized int read(byte[] buffer, int off, int len) throws IOException { // Too implementation-linked, but... - for (int i = 0; i < chunks[chunk].length(); ++i) { - buffer[i] = (byte) chunks[chunk].charAt(i); + for (int i = 0; i < this.chunks[this.chunk].length(); ++i) { + buffer[i] = (byte) this.chunks[this.chunk].charAt(i); } - return chunks[chunk++].length(); + return this.chunks[this.chunk++].length(); } } + + @org.junit.Test + public void thatChunkedContentIsChunked() throws Exception { + PipedInputStream pipedInputStream = new ChunkedInputStream(new String[]{ + "some", + "thing which is longer than sixteen characters", + "whee!", + "" + }); + String[] expected = { + "HTTP/1.1 200 OK", + "Content-Type: what/ever", + "Date: .*", + "Connection: keep-alive", + "Transfer-Encoding: chunked", + "", + "4", + "some", + "2d", + "thing which is longer than sixteen characters", + "5", + "whee!", + "0", + "" + }; + this.testServer.response = new NanoHTTPD(0) { + }.newChunkedResponse(OK, "what/ever", pipedInputStream); + this.testServer.response.setChunkedTransfer(true); + + ByteArrayOutputStream byteArrayOutputStream = invokeServer("GET / HTTP/1.1"); + + assertResponse(byteArrayOutputStream, expected); + } } diff --git a/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java index 517ad29..8ce49e1 100644 --- a/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java @@ -1,102 +1,132 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ import java.io.ByteArrayOutputStream; import java.io.InputStream; -import java.util.List; -import static junit.framework.Assert.*; +import org.junit.Test; public class HttpDeleteRequestTest extends HttpServerTest { @Test public void testDeleteRequestThatDoesntSendBackResponseBody_EmptyString() throws Exception { - testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, ""); + this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, ""); - ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1"); + ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1"); String[] expected = { - "HTTP/1.1 204 No Content", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 204 No Content", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; assertResponse(outputStream, expected); } @Test - public void testDeleteRequestThatDoesntSendBackResponseBody_NullString() throws Exception { - testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (String)null); + public void testDeleteRequestThatDoesntSendBackResponseBody_NullInputStream() throws Exception { + this.testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (InputStream) null); - ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1"); + ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1"); String[] expected = { - "HTTP/1.1 204 No Content", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 204 No Content", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; assertResponse(outputStream, expected); } @Test - public void testDeleteRequestThatDoesntSendBackResponseBody_NullInputStream() throws Exception { - testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (InputStream)null); + public void testDeleteRequestThatDoesntSendBackResponseBody_NullString() throws Exception { + this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (String) null); - ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1"); + ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1"); String[] expected = { - "HTTP/1.1 204 No Content", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 204 No Content", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; assertResponse(outputStream, expected); } @Test - public void testDeleteRequestThatSendsBackResponseBody_Success() throws Exception { - testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/xml", "<body />"); + public void testDeleteRequestThatSendsBackResponseBody_Accepted() throws Exception { + this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.ACCEPTED, "application/xml", "<body />"); - ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1"); + ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1"); String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: application/xml", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 8", - "", - "<body />" + "HTTP/1.1 202 Accepted", + "Content-Type: application/xml", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 8", + "", + "<body />" }; assertResponse(outputStream, expected); } @Test - public void testDeleteRequestThatSendsBackResponseBody_Accepted() throws Exception { - testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.ACCEPTED, "application/xml", "<body />"); + public void testDeleteRequestThatSendsBackResponseBody_Success() throws Exception { + this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/xml", "<body />"); - ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1"); + ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1"); String[] expected = { - "HTTP/1.1 202 Accepted", - "Content-Type: application/xml", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 8", - "", - "<body />" + "HTTP/1.1 200 OK", + "Content-Type: application/xml", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 8", + "", + "<body />" }; assertResponse(outputStream, expected); diff --git a/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java index 598e50b..e1a6b8b 100644 --- a/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java @@ -1,171 +1,208 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.util.List; -import static junit.framework.Assert.*; +import org.junit.Test; public class HttpGetRequestTest extends HttpServerTest { @Test - public void testFullyQualifiedWorkingGetRequest() throws Exception { - ByteArrayOutputStream outputStream = invokeServer("GET " + URI + " HTTP/1.1"); - - String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" - }; - - assertResponse(outputStream, expected); + public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() { + invokeServer("GET " + HttpServerTest.URI + "?foo&bar= HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("foo").size()); + assertTrue(this.testServer.decodedParamters.get("bar") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("bar").size()); + assertEquals("", this.testServer.decodedParamters.get("bar").get(0)); } @Test - public void testOutputOfServeSentBackToClient() throws Exception { - String responseBody = "Success!"; - testServer.response = new NanoHTTPD.Response(responseBody); - ByteArrayOutputStream outputStream = invokeServer("GET " + URI + " HTTP/1.1"); - - String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 8", - "", - responseBody - }; + public void testDecodingMixtureOfParameters() { + invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(2, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1)); + assertTrue(this.testServer.decodedParamters.get("zot") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("zot").size()); + assertTrue(this.testServer.decodedParamters.get("zim") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("zim").size()); + assertEquals("", this.testServer.decodedParamters.get("zim").get(0)); + } - assertResponse(outputStream, expected); + @Test + public void testDecodingParametersFromParameterMap() { + invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); + assertEquals(this.testServer.decodedParamters, this.testServer.decodedParamtersFromParameter); } + // -------------------------------------------------------------------------------------------------------- + // // + @Test - public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() { - invokeServer("GET " + URI + " HTTP/1.1"); - assertNotNull(testServer.parms); - assertNotNull(testServer.header); - assertNotNull(testServer.files); - assertNotNull(testServer.uri); + public void testDecodingParametersWithSingleValue() { + invokeServer("GET " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1"); + assertEquals("foo=bar&baz=zot", this.testServer.queryParameterString); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertTrue(this.testServer.decodedParamters.get("baz") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("baz").size()); + assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0)); } @Test - public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { - String userAgent = "jUnit 4.8.2 Unit Test"; - invokeServer("GET " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n"); - assertEquals(userAgent, testServer.header.get("user-agent")); - assertEquals(NanoHTTPD.Method.GET, testServer.method); - assertEquals(URI, testServer.uri); + public void testDecodingParametersWithSingleValueAndMissingValue() { + invokeServer("GET " + HttpServerTest.URI + "?foo&baz=zot HTTP/1.1"); + assertEquals("foo&baz=zot", this.testServer.queryParameterString); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("foo").size()); + assertTrue(this.testServer.decodedParamters.get("baz") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("baz").size()); + assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0)); } @Test - public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { - String userAgent = "jUnit 4.8.2 Unit Test"; - String accept = "text/html"; - invokeServer("GET " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept); - assertEquals(userAgent, testServer.header.get("user-agent")); - assertEquals(accept, testServer.header.get("accept")); + public void testDecodingSingleFieldRepeated() { + invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(2, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1)); } @Test - public void testSingleGetParameter() { - invokeServer("GET " + URI + "?foo=bar HTTP/1.1"); - assertEquals("bar", testServer.parms.get("foo")); + public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() { + invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1"); + assertNotNull(this.testServer.parms); + assertNotNull(this.testServer.header); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); } @Test - public void testSingleGetParameterWithNoValue() { - invokeServer("GET " + URI + "?foo HTTP/1.1"); - assertEquals("", testServer.parms.get("foo")); + public void testFullyQualifiedWorkingGetRequest() throws Exception { + ByteArrayOutputStream outputStream = invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1"); + + String[] expected = { + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" + }; + + assertResponse(outputStream, expected); } @Test public void testMultipleGetParameters() { - invokeServer("GET " + URI + "?foo=bar&baz=zot HTTP/1.1"); - assertEquals("bar", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); + invokeServer("GET " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1"); + assertEquals("bar", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); } @Test public void testMultipleGetParametersWithMissingValue() { - invokeServer("GET " + URI + "?foo=&baz=zot HTTP/1.1"); - assertEquals("", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); + invokeServer("GET " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1"); + assertEquals("", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); } @Test public void testMultipleGetParametersWithMissingValueAndRequestHeaders() { - invokeServer("GET " + URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html"); - assertEquals("", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); - assertEquals("text/html", testServer.header.get("accept")); + invokeServer("GET " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html"); + assertEquals("", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); + assertEquals("text/html", this.testServer.header.get("accept")); } @Test - public void testDecodingParametersWithSingleValue() { - invokeServer("GET " + URI + "?foo=bar&baz=zot HTTP/1.1"); - assertEquals("foo=bar&baz=zot", testServer.queryParameterString); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(1, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertTrue(testServer.decodedParamters.get("baz") instanceof List); - assertEquals(1, testServer.decodedParamters.get("baz").size()); - assertEquals("zot", testServer.decodedParamters.get("baz").get(0)); + public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { + String userAgent = "jUnit 4.8.2 Unit Test"; + String accept = "text/html"; + invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept); + assertEquals(userAgent, this.testServer.header.get("user-agent")); + assertEquals(accept, this.testServer.header.get("accept")); } @Test - public void testDecodingParametersWithSingleValueAndMissingValue() { - invokeServer("GET " + URI + "?foo&baz=zot HTTP/1.1"); - assertEquals("foo&baz=zot", testServer.queryParameterString); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(0, testServer.decodedParamters.get("foo").size()); - assertTrue(testServer.decodedParamters.get("baz") instanceof List); - assertEquals(1, testServer.decodedParamters.get("baz").size()); - assertEquals("zot", testServer.decodedParamters.get("baz").get(0)); - } + public void testOutputOfServeSentBackToClient() throws Exception { + String responseBody = "Success!"; + this.testServer.response = NanoHTTPD.newFixedLengthResponse(responseBody); + ByteArrayOutputStream outputStream = invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1"); - @Test - public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() { - invokeServer("GET " + URI + "?foo&bar= HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(0, testServer.decodedParamters.get("foo").size()); - assertTrue(testServer.decodedParamters.get("bar") instanceof List); - assertEquals(1, testServer.decodedParamters.get("bar").size()); - assertEquals("", testServer.decodedParamters.get("bar").get(0)); + String[] expected = { + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 8", + "", + responseBody + }; + + assertResponse(outputStream, expected); } @Test - public void testDecodingSingleFieldRepeated() { - invokeServer("GET " + URI + "?foo=bar&foo=baz HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(2, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertEquals("baz", testServer.decodedParamters.get("foo").get(1)); + public void testSingleGetParameter() { + invokeServer("GET " + HttpServerTest.URI + "?foo=bar HTTP/1.1"); + assertEquals("bar", this.testServer.parms.get("foo")); } @Test - public void testDecodingMixtureOfParameters() { - invokeServer("GET " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(2, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertEquals("baz", testServer.decodedParamters.get("foo").get(1)); - assertTrue(testServer.decodedParamters.get("zot") instanceof List); - assertEquals(0, testServer.decodedParamters.get("zot").size()); - assertTrue(testServer.decodedParamters.get("zim") instanceof List); - assertEquals(1, testServer.decodedParamters.get("zim").size()); - assertEquals("", testServer.decodedParamters.get("zim").get(0)); + public void testSingleGetParameterWithNoValue() { + invokeServer("GET " + HttpServerTest.URI + "?foo HTTP/1.1"); + assertEquals("", this.testServer.parms.get("foo")); } @Test - public void testDecodingParametersFromParameterMap() { - invokeServer("GET " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); - assertEquals(testServer.decodedParamters, testServer.decodedParamtersFromParameter); + public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { + String userAgent = "jUnit 4.8.2 Unit Test"; + invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n"); + assertEquals(userAgent, this.testServer.header.get("user-agent")); + assertEquals(NanoHTTPD.Method.GET, this.testServer.method); + assertEquals(HttpServerTest.URI, this.testServer.uri); } - // -------------------------------------------------------------------------------------------------------- // } diff --git a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java index 1c5901f..9b5983e 100644 --- a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java @@ -1,158 +1,196 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.util.List; -import static junit.framework.Assert.*; +import org.junit.Test; public class HttpHeadRequestTest extends HttpServerTest { + @Override - public void setUp() { + public void setUp() throws Exception { super.setUp(); String responseBody = "Success!"; - testServer.response = new NanoHTTPD.Response(responseBody); + this.testServer.response = NanoHTTPD.newFixedLengthResponse(responseBody); } @Test - public void testHeadRequestDoesntSendBackResponseBody() throws Exception { - ByteArrayOutputStream outputStream = invokeServer("HEAD " + URI + " HTTP/1.1"); - - String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 8", - "" - }; - - assertResponse(outputStream, expected); + public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo&bar= HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("foo").size()); + assertTrue(this.testServer.decodedParamters.get("bar") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("bar").size()); + assertEquals("", this.testServer.decodedParamters.get("bar").get(0)); } @Test - public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() { - invokeServer("HEAD " + URI + " HTTP/1.1"); - assertNotNull(testServer.parms); - assertNotNull(testServer.header); - assertNotNull(testServer.files); - assertNotNull(testServer.uri); + public void testDecodingMixtureOfParameters() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(2, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1)); + assertTrue(this.testServer.decodedParamters.get("zot") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("zot").size()); + assertTrue(this.testServer.decodedParamters.get("zim") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("zim").size()); + assertEquals("", this.testServer.decodedParamters.get("zim").get(0)); } @Test - public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { - String userAgent = "jUnit 4.8.2 Unit Test"; - invokeServer("HEAD " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n"); - assertEquals(userAgent, testServer.header.get("user-agent")); - assertEquals(NanoHTTPD.Method.HEAD, testServer.method); - assertEquals(URI, testServer.uri); + public void testDecodingParametersFromParameterMap() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); + assertEquals(this.testServer.decodedParamters, this.testServer.decodedParamtersFromParameter); } + // -------------------------------------------------------------------------------------------------------- + // // + @Test - public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { - String userAgent = "jUnit 4.8.2 Unit Test"; - String accept = "text/html"; - invokeServer("HEAD " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept); - assertEquals(userAgent, testServer.header.get("user-agent")); - assertEquals(accept, testServer.header.get("accept")); + public void testDecodingParametersWithSingleValue() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1"); + assertEquals("foo=bar&baz=zot", this.testServer.queryParameterString); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertTrue(this.testServer.decodedParamters.get("baz") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("baz").size()); + assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0)); } @Test - public void testSingleGetParameter() { - invokeServer("HEAD " + URI + "?foo=bar HTTP/1.1"); - assertEquals("bar", testServer.parms.get("foo")); + public void testDecodingParametersWithSingleValueAndMissingValue() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo&baz=zot HTTP/1.1"); + assertEquals("foo&baz=zot", this.testServer.queryParameterString); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(0, this.testServer.decodedParamters.get("foo").size()); + assertTrue(this.testServer.decodedParamters.get("baz") instanceof List); + assertEquals(1, this.testServer.decodedParamters.get("baz").size()); + assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0)); } @Test - public void testSingleGetParameterWithNoValue() { - invokeServer("HEAD " + URI + "?foo HTTP/1.1"); - assertEquals("", testServer.parms.get("foo")); + public void testDecodingSingleFieldRepeated() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz HTTP/1.1"); + assertTrue(this.testServer.decodedParamters.get("foo") instanceof List); + assertEquals(2, this.testServer.decodedParamters.get("foo").size()); + assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0)); + assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1)); } @Test - public void testMultipleGetParameters() { - invokeServer("HEAD " + URI + "?foo=bar&baz=zot HTTP/1.1"); - assertEquals("bar", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); + public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() { + invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1"); + assertNotNull(this.testServer.parms); + assertNotNull(this.testServer.header); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); } @Test - public void testMultipleGetParametersWithMissingValue() { - invokeServer("HEAD " + URI + "?foo=&baz=zot HTTP/1.1"); - assertEquals("", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); + public void testHeadRequestDoesntSendBackResponseBody() throws Exception { + ByteArrayOutputStream outputStream = invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1"); + + String[] expected = { + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 8", + "" + }; + + assertResponse(outputStream, expected); } @Test - public void testMultipleGetParametersWithMissingValueAndRequestHeaders() { - invokeServer("HEAD " + URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html"); - assertEquals("", testServer.parms.get("foo")); - assertEquals("zot", testServer.parms.get("baz")); - assertEquals("text/html", testServer.header.get("accept")); + public void testMultipleGetParameters() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1"); + assertEquals("bar", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); } @Test - public void testDecodingParametersWithSingleValue() { - invokeServer("HEAD " + URI + "?foo=bar&baz=zot HTTP/1.1"); - assertEquals("foo=bar&baz=zot", testServer.queryParameterString); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(1, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertTrue(testServer.decodedParamters.get("baz") instanceof List); - assertEquals(1, testServer.decodedParamters.get("baz").size()); - assertEquals("zot", testServer.decodedParamters.get("baz").get(0)); + public void testMultipleGetParametersWithMissingValue() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1"); + assertEquals("", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); } @Test - public void testDecodingParametersWithSingleValueAndMissingValue() { - invokeServer("HEAD " + URI + "?foo&baz=zot HTTP/1.1"); - assertEquals("foo&baz=zot", testServer.queryParameterString); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(0, testServer.decodedParamters.get("foo").size()); - assertTrue(testServer.decodedParamters.get("baz") instanceof List); - assertEquals(1, testServer.decodedParamters.get("baz").size()); - assertEquals("zot", testServer.decodedParamters.get("baz").get(0)); + public void testMultipleGetParametersWithMissingValueAndRequestHeaders() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html"); + assertEquals("", this.testServer.parms.get("foo")); + assertEquals("zot", this.testServer.parms.get("baz")); + assertEquals("text/html", this.testServer.header.get("accept")); } @Test - public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() { - invokeServer("HEAD " + URI + "?foo&bar= HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(0, testServer.decodedParamters.get("foo").size()); - assertTrue(testServer.decodedParamters.get("bar") instanceof List); - assertEquals(1, testServer.decodedParamters.get("bar").size()); - assertEquals("", testServer.decodedParamters.get("bar").get(0)); + public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { + String userAgent = "jUnit 4.8.2 Unit Test"; + String accept = "text/html"; + invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept); + assertEquals(userAgent, this.testServer.header.get("user-agent")); + assertEquals(accept, this.testServer.header.get("accept")); } @Test - public void testDecodingSingleFieldRepeated() { - invokeServer("HEAD " + URI + "?foo=bar&foo=baz HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(2, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertEquals("baz", testServer.decodedParamters.get("foo").get(1)); + public void testSingleGetParameter() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar HTTP/1.1"); + assertEquals("bar", this.testServer.parms.get("foo")); } @Test - public void testDecodingMixtureOfParameters() { - invokeServer("HEAD " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); - assertTrue(testServer.decodedParamters.get("foo") instanceof List); - assertEquals(2, testServer.decodedParamters.get("foo").size()); - assertEquals("bar", testServer.decodedParamters.get("foo").get(0)); - assertEquals("baz", testServer.decodedParamters.get("foo").get(1)); - assertTrue(testServer.decodedParamters.get("zot") instanceof List); - assertEquals(0, testServer.decodedParamters.get("zot").size()); - assertTrue(testServer.decodedParamters.get("zim") instanceof List); - assertEquals(1, testServer.decodedParamters.get("zim").size()); - assertEquals("", testServer.decodedParamters.get("zim").get(0)); + public void testSingleGetParameterWithNoValue() { + invokeServer("HEAD " + HttpServerTest.URI + "?foo HTTP/1.1"); + assertEquals("", this.testServer.parms.get("foo")); } @Test - public void testDecodingParametersFromParameterMap() { - invokeServer("HEAD " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1"); - assertEquals(testServer.decodedParamters, testServer.decodedParamtersFromParameter); + public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() { + String userAgent = "jUnit 4.8.2 Unit Test"; + invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n"); + assertEquals(userAgent, this.testServer.header.get("user-agent")); + assertEquals(NanoHTTPD.Method.HEAD, this.testServer.method); + assertEquals(HttpServerTest.URI, this.testServer.uri); } - // -------------------------------------------------------------------------------------------------------- // } diff --git a/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java b/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java index f349ee5..e168814 100644 --- a/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java @@ -1,5 +1,38 @@ package fi.iki.elonen; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + import static junit.framework.Assert.fail; import java.io.ByteArrayOutputStream; @@ -10,68 +43,96 @@ import org.junit.Test; public class HttpKeepAliveTest extends HttpServerTest { + private Throwable error = null; + @Test public void testManyGetRequests() throws Exception { - String request = "GET " + URI + " HTTP/1.1\r\n\r\n"; + String request = "GET " + HttpServerTest.URI + " HTTP/1.1\r\n\r\n"; String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; testManyRequests(request, expected); } - + @Test public void testManyPutRequests() throws Exception { String data = "BodyData 1\nLine 2"; - String request = "PUT " + URI + " HTTP/1.1\r\nContent-Length: " + data.length() + "\r\n\r\n" + data; + String request = "PUT " + HttpServerTest.URI + " HTTP/1.1\r\nContent-Length: " + data.length() + "\r\n\r\n" + data; String[] expected = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; testManyRequests(request, expected); } - private Throwable error = null; - /** - * Issue the given request many times to check whether an error occurs. - * For this test, a small stack size is used, since a stack overflow is among the possible errors. - * @param request The request to issue - * @param expected The expected response + * Issue the given request many times to check whether an error occurs. For + * this test, a small stack size is used, since a stack overflow is among + * the possible errors. + * + * @param request + * The request to issue + * @param expected + * The expected response */ public void testManyRequests(final String request, final String[] expected) throws Exception { Runnable r = new Runnable() { + + @Override public void run() { try { PipedOutputStream requestStream = new PipedOutputStream(); PipedInputStream inputStream = new PipedInputStream(requestStream); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - NanoHTTPD.HTTPSession session = testServer.createSession(new TestTempFileManager(), inputStream, outputStream); - for (int i = 0; i < 2048; i++) { - requestStream.write(request.getBytes()); + NanoHTTPD.DefaultTempFileManager tempFileManager = new NanoHTTPD.DefaultTempFileManager(); + try { + NanoHTTPD.HTTPSession session = HttpKeepAliveTest.this.testServer.createSession(tempFileManager, inputStream, outputStream); + for (int i = 0; i < 2048; i++) { + requestStream.write(request.getBytes()); + requestStream.flush(); + outputStream.reset(); + session.execute(); + assertResponse(outputStream, expected); + } + + // Finally, try "Connection: Close" + String closeReq = request.replaceAll("HTTP/1.1", "HTTP/1.1\r\nConnection: Close"); + expected[3] = "Connection: close"; + requestStream.write(closeReq.getBytes()); + outputStream.reset(); requestStream.flush(); - session.execute(); + // Server should now close the socket by throwing a + // SocketException: + try { + session.execute(); + } catch (java.net.SocketException se) { + junit.framework.Assert.assertEquals(se.getMessage(), "NanoHttpd Shutdown"); + } assertResponse(outputStream, expected); + + } finally { + tempFileManager.clear(); } } catch (Throwable t) { - error = t; + HttpKeepAliveTest.this.error = t; } } }; Thread t = new Thread(null, r, "Request Thread", 1 << 17); t.start(); t.join(); - if (error != null) { - fail(""+error); - error.printStackTrace(); + if (this.error != null) { + fail("" + this.error); + this.error.printStackTrace(); } } } diff --git a/core/src/test/java/fi/iki/elonen/HttpParsingTest.java b/core/src/test/java/fi/iki/elonen/HttpParsingTest.java index 9705c5b..1f40f63 100644 --- a/core/src/test/java/fi/iki/elonen/HttpParsingTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpParsingTest.java @@ -1,32 +1,63 @@ package fi.iki.elonen; -import org.junit.Test; - -import java.net.URLDecoder; -import java.net.URLEncoder; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ import static junit.framework.Assert.assertEquals; +import org.junit.Test; + public class HttpParsingTest extends HttpServerTest { + + @Test + public void testMultibyteCharacterSupport() throws Exception { + String expected = "Chinese \u738b Letters"; + String input = "Chinese+%e7%8e%8b+Letters"; + assertEquals(expected, this.testServer.decodePercent(input)); + } + @Test public void testNormalCharacters() throws Exception { for (int i = 0x20; i < 0x80; i++) { String hex = Integer.toHexString(i); String input = "%" + hex; char expected = (char) i; - assertEquals("" + expected, testServer.decodePercent(input)); + assertEquals("" + expected, this.testServer.decodePercent(input)); } } @Test - public void testMultibyteCharacterSupport() throws Exception { - String expected = "Chinese \u738b Letters"; - String input = "Chinese+%e7%8e%8b+Letters"; - assertEquals(expected, testServer.decodePercent(input)); - } - - @Test public void testPlusInQueryParams() throws Exception { - assertEquals("foo bar", testServer.decodePercent("foo+bar")); + assertEquals("foo bar", this.testServer.decodePercent("foo+bar")); } } diff --git a/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java index 3cb37e0..db82e1c 100644 --- a/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java @@ -1,157 +1,184 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static junit.framework.Assert.assertEquals; import java.io.BufferedReader; import java.io.FileReader; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.UUID; -import static junit.framework.Assert.assertEquals; +import org.junit.Test; public class HttpPostRequestTest extends HttpServerTest { public static final String CONTENT_LENGTH = "Content-Length: "; + public static final String FIELD = "caption"; + public static final String VALUE = "Summer vacation"; + public static final String FIELD2 = "location"; + public static final String VALUE2 = "Grand Canyon"; + public static final String POST_RAW_CONTENT_FILE_ENTRY = "postData"; + public static final String VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS = "Test raw data & Result value"; - @Test - public void testSimpleRawPostData() throws Exception { - String header = "POST " + URI + " HTTP/1.1\n"; - String content = VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS + "\n"; + /** + * contains common preparation steps for testing POST with Multipart Form + * + * @param fileName + * Name of file to be uploaded + * @param fileContent + * Content of file to be uploaded + * @return input String with POST request complete information including + * header, length and content + */ + private String preparePostWithMultipartForm(String fileName, String fileContent) { + String divider = UUID.randomUUID().toString(); + String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n"; + String content = + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"; filename=\"" + fileName + "\"\r\n" + + "Content-Type: image/jpeg\r\n" + "\r\n" + fileContent + "\r\n" + "--" + divider + "--\r\n"; int size = content.length() + header.length(); int contentLengthHeaderValueSize = String.valueOf(size).length(); - int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length(); - String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content; + int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); + String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 5) + "\r\n\r\n" + content; + + return input; + } + + @Test + public void testPostWithMultipartFormUpload() throws Exception { + String filename = "GrandCanyon.txt"; + String fileContent = HttpPostRequestTest.VALUE; + String input = preparePostWithMultipartForm(filename, fileContent); + invokeServer(input); - assertEquals(0, testServer.parms.size()); - assertEquals(1, testServer.files.size()); - assertEquals(VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS, testServer.files.get(POST_RAW_CONTENT_FILE_ENTRY)); + + assertEquals(1, this.testServer.parms.size()); + BufferedReader reader = new BufferedReader(new FileReader(this.testServer.files.get(HttpPostRequestTest.FIELD))); + List<String> lines = readLinesFromFile(reader); + assertLinesOfText(new String[]{ + fileContent + }, lines); } @Test - public void testSimplePostWithSingleMultipartFormField() throws Exception { - String divider = UUID.randomUUID().toString(); - String header = "POST " + URI + " HTTP/1.1\nContent-Type: " + - "multipart/form-data; boundary=" + divider + "\n"; - String content = "--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD+"\"\n" + - "\n" + - VALUE +"\n" + - "--" + divider + "--\n"; - int size = content.length() + header.length(); - int contentLengthHeaderValueSize = String.valueOf(size).length(); - int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length(); - String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content; + public void testPostWithMultipartFormUploadFilenameHasSpaces() throws Exception { + String fileNameWithSpace = "Grand Canyon.txt"; + String fileContent = HttpPostRequestTest.VALUE; + String input = preparePostWithMultipartForm(fileNameWithSpace, fileContent); + invokeServer(input); - assertEquals(1, testServer.parms.size()); - assertEquals(VALUE, testServer.parms.get(FIELD)); + String fileNameAfter = new ArrayList<String>(this.testServer.parms.values()).get(0); + + assertEquals(fileNameWithSpace, fileNameAfter); } @Test public void testPostWithMultipleMultipartFormFields() throws Exception { String divider = UUID.randomUUID().toString(); - String header = "POST " + URI + " HTTP/1.1\nContent-Type: " + - "multipart/form-data; boundary=" + divider + "\n"; - String content = "--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD+"\"\n" + - "\n" + - VALUE +"\n" +"--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD2+"\"\n" + - "\n" + - VALUE2 +"\n" + - "--" + divider + "--\n"; + String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\n"; + String content = + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" + + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2 + + "\r\n" + "--" + divider + "--\r\n"; int size = content.length() + header.length(); int contentLengthHeaderValueSize = String.valueOf(size).length(); - int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length(); - String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content; + int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); + String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; invokeServer(input); - assertEquals(2, testServer.parms.size()); - assertEquals(VALUE, testServer.parms.get(FIELD)); - assertEquals(VALUE2, testServer.parms.get(FIELD2)); + assertEquals(2, this.testServer.parms.size()); + assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); + assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2)); } @Test public void testPostWithMultipleMultipartFormFieldsWhereContentTypeWasSeparatedByComma() throws Exception { String divider = UUID.randomUUID().toString(); - String header = "POST " + URI + " HTTP/1.1\nContent-Type: " + - "multipart/form-data, boundary=" + divider + "\n"; - String content = "--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD+"\"\n" + - "\n" + - VALUE +"\n" +"--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD2+"\"\n" + - "\n" + - VALUE2 +"\n" + - "--" + divider + "--\n"; + String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n"; + String content = + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" + + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2 + + "\r\n" + "--" + divider + "--\r\n"; int size = content.length() + header.length(); int contentLengthHeaderValueSize = String.valueOf(size).length(); - int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length(); - String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content; + int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); + String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; invokeServer(input); - assertEquals(2, testServer.parms.size()); - assertEquals(VALUE, testServer.parms.get(FIELD)); - assertEquals(VALUE2, testServer.parms.get(FIELD2)); + assertEquals(2, this.testServer.parms.size()); + assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); + assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2)); } - + @Test - public void testPostWithMultipartFormUpload() throws Exception { - String filename = "GrandCanyon.txt"; - String fileContent = VALUE; - String input = preparePostWithMultipartForm(filename, fileContent); - + public void testSimplePostWithSingleMultipartFormField() throws Exception { + String divider = UUID.randomUUID().toString(); + String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\r\n"; + String content = + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" + + "--" + divider + "--\r\n"; + int size = content.length() + header.length(); + int contentLengthHeaderValueSize = String.valueOf(size).length(); + int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); + String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; invokeServer(input); - - assertEquals(1, testServer.parms.size()); - BufferedReader reader = new BufferedReader(new FileReader(testServer.files.get(FIELD))); - List<String> lines = readLinesFromFile(reader); - assertLinesOfText(new String[]{fileContent}, lines); + + assertEquals(1, this.testServer.parms.size()); + assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); } - + @Test - public void testPostWithMultipartFormUploadFilenameHasSpaces() throws Exception { - String fileNameWithSpace = "Grand Canyon.txt"; - String fileContent = VALUE; - String input = preparePostWithMultipartForm(fileNameWithSpace, fileContent); - - invokeServer(input); - - String fileNameAfter = new ArrayList<String>(testServer.parms.values()).get(0); - - assertEquals(fileNameWithSpace, fileNameAfter); - } - - /** - * contains common preparation steps for testing POST with Multipart Form - * @param fileName Name of file to be uploaded - * @param fileContent Content of file to be uploaded - * @return input String with POST request complete information including header, length and content - */ - private String preparePostWithMultipartForm(String fileName, String fileContent) { - String divider = UUID.randomUUID().toString(); - String header = "POST " + URI + " HTTP/1.1\nContent-Type: " + - "multipart/form-data, boundary=" + divider + "\n"; - String content = "--" + divider + "\n" + - "Content-Disposition: form-data; name=\""+FIELD+"\"; filename=\""+fileName+"\"\n" + - "Content-Type: image/jpeg\r\n"+ - "\r\n" + - fileContent +"\r\n" + - "--" + divider + "--\n"; + public void testSimpleRawPostData() throws Exception { + String header = "POST " + HttpServerTest.URI + " HTTP/1.1\n"; + String content = HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS + "\r\n"; int size = content.length() + header.length(); int contentLengthHeaderValueSize = String.valueOf(size).length(); - int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length(); - String input = header + CONTENT_LENGTH + (contentLength+5) + "\r\n\r\n" + content; - - return input; + int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); + String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; + invokeServer(input); + assertEquals(0, this.testServer.parms.size()); + assertEquals(1, this.testServer.files.size()); + assertEquals(HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS, this.testServer.files.get(HttpPostRequestTest.POST_RAW_CONTENT_FILE_ENTRY)); } } diff --git a/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java index 912113e..54a3343 100644 --- a/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java @@ -1,39 +1,72 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static junit.framework.Assert.assertTrue; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.FileReader; import java.util.List; -import static junit.framework.Assert.*; +import org.junit.Test; public class HttpPutRequestTest extends HttpServerTest { @Test public void testPutRequestSendsContent() throws Exception { - ByteArrayOutputStream outputStream = invokeServer("PUT " + URI + " HTTP/1.1\r\n\r\nBodyData 1\nLine 2"); + ByteArrayOutputStream outputStream = invokeServer("PUT " + HttpServerTest.URI + " HTTP/1.1\r\n\r\nBodyData 1\nLine 2"); String[] expectedOutput = { - "HTTP/1.1 200 OK", - "Content-Type: text/html", - "Date: .*", - "Connection: keep-alive", - "Content-Length: 0", - "" + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Date: .*", + "Connection: keep-alive", + "Content-Length: 0", + "" }; assertResponse(outputStream, expectedOutput); - assertTrue(testServer.files.containsKey("content")); + assertTrue(this.testServer.files.containsKey("content")); BufferedReader reader = null; try { String[] expectedInputToServeMethodViaFile = { - "BodyData 1", - "Line 2" + "BodyData 1", + "Line 2" }; - reader = new BufferedReader(new FileReader(testServer.files.get("content"))); + reader = new BufferedReader(new FileReader(this.testServer.files.get("content"))); List<String> lines = readLinesFromFile(reader); assertLinesOfText(expectedInputToServeMethodViaFile, lines); } finally { diff --git a/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java new file mode 100644 index 0000000..30fb48c --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java @@ -0,0 +1,89 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.File; +import java.io.IOException; + +import javax.net.ssl.SSLContext; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class HttpSSLServerTest extends HttpServerTest { + + @Test + public void testSSLConnection() throws ClientProtocolException, IOException { + DefaultHttpClient httpclient = new DefaultHttpClient(); + HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html"); + HttpResponse response = httpclient.execute(httphead); + HttpEntity entity = response.getEntity(); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + + Assert.assertEquals(9043, this.testServer.getListeningPort()); + Assert.assertTrue(this.testServer.isAlive()); + } + + @Before + public void setUp() throws Exception { + System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath()); + this.testServer = new TestServer(9043); + this.testServer.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null); + this.tempFileManager = new TestTempFileManager(); + this.testServer.start(); + try { + long start = System.currentTimeMillis(); + Thread.sleep(100L); + while (!this.testServer.wasStarted()) { + Thread.sleep(100L); + if (System.currentTimeMillis() - start > 2000) { + Assert.fail("could not start server"); + } + } + } catch (InterruptedException e) { + } + } + + @After + public void tearDown() { + this.testServer.stop(); + } +} diff --git a/core/src/test/java/fi/iki/elonen/HttpServerTest.java b/core/src/test/java/fi/iki/elonen/HttpServerTest.java index ef209d1..e8be61e 100644 --- a/core/src/test/java/fi/iki/elonen/HttpServerTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpServerTest.java @@ -1,41 +1,143 @@ package fi.iki.elonen; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ -import java.io.*; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; import java.net.InetAddress; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import static junit.framework.Assert.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; /** - * @author Paul S. Hawke (paul.hawke@gmail.com) - * On: 3/10/13 at 8:32 PM + * @author Paul S. Hawke (paul.hawke@gmail.com) On: 3/10/13 at 8:32 PM */ public class HttpServerTest { - public static final String URI = "http://www.myserver.org/pub/WWW/someFile.html"; - protected TestServer testServer; - private TestTempFileManager tempFileManager; - @Before - public void setUp() { - testServer = new TestServer(); - tempFileManager = new TestTempFileManager(); + public static class TestServer extends NanoHTTPD { + + public Response response = newFixedLengthResponse(""); + + public String uri; + + public Method method; + + public Map<String, String> header; + + public Map<String, String> parms; + + public Map<String, String> files; + + public Map<String, List<String>> decodedParamters; + + public Map<String, List<String>> decodedParamtersFromParameter; + + public String queryParameterString; + + public TestServer() { + super(8192); + } + + public TestServer(int port) { + super(port); + } + + public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + return new HTTPSession(tempFileManager, inputStream, outputStream); + } + + public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress); + } + + @Override + public Response serve(IHTTPSession session) { + this.uri = session.getUri(); + this.method = session.getMethod(); + this.header = session.getHeaders(); + this.parms = session.getParms(); + this.files = new HashMap<String, String>(); + try { + session.parseBody(this.files); + } catch (Exception e) { + e.printStackTrace(); + } + this.queryParameterString = session.getQueryParameterString(); + this.decodedParamtersFromParameter = decodeParameters(this.queryParameterString); + this.decodedParamters = decodeParameters(session.getQueryParameterString()); + return this.response; + } } - @After - public void tearDown() { - tempFileManager._clear(); + public static class TestTempFileManager extends NanoHTTPD.DefaultTempFileManager { + + public void _clear() { + super.clear(); + } + + @Override + public void clear() { + // ignore + } } - @Test - public void testServerExists() { - assertNotNull(testServer); + public static final String URI = "http://www.myserver.org/pub/WWW/someFile.html"; + + protected TestServer testServer; + + protected TestTempFileManager tempFileManager; + + protected void assertLinesOfText(String[] expected, List<String> lines) { + // assertEquals(expected.length, lines.size()); + for (int i = 0; i < expected.length; i++) { + String line = lines.get(i); + assertTrue("Output line " + i + " doesn't match expectation.\n" + " Output: " + line + "\n" + "Expected: " + expected[i], line.matches(expected[i])); + } } protected void assertResponse(ByteArrayOutputStream outputStream, String[] expected) throws IOException { @@ -43,34 +145,24 @@ public class HttpServerTest { assertLinesOfText(expected, lines); } - protected void assertLinesOfText(String[] expected, List<String> lines) { -// assertEquals(expected.length, lines.size()); - for (int i = 0; i < expected.length; i++) { - String line = lines.get(i); - assertTrue("Output line " + i + " doesn't match expectation.\n" + - " Output: " + line + "\n" + - "Expected: " + expected[i], line.matches(expected[i])); - } + protected List<String> getOutputLines(ByteArrayOutputStream outputStream) throws IOException { + BufferedReader reader = new BufferedReader(new StringReader(outputStream.toString())); + return readLinesFromFile(reader); } protected ByteArrayOutputStream invokeServer(String request) { ByteArrayInputStream inputStream = new ByteArrayInputStream(request.getBytes()); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - NanoHTTPD.HTTPSession session = testServer.createSession(tempFileManager, inputStream, outputStream); + NanoHTTPD.HTTPSession session = this.testServer.createSession(this.tempFileManager, inputStream, outputStream); try { session.execute(); } catch (IOException e) { - fail(""+e); + fail("" + e); e.printStackTrace(); } return outputStream; } - protected List<String> getOutputLines(ByteArrayOutputStream outputStream) throws IOException { - BufferedReader reader = new BufferedReader(new StringReader(outputStream.toString())); - return readLinesFromFile(reader); - } - protected List<String> readLinesFromFile(BufferedReader reader) throws IOException { List<String> lines = new ArrayList<String>(); String line = ""; @@ -83,60 +175,19 @@ public class HttpServerTest { return lines; } - public static class TestTempFileManager extends NanoHTTPD.DefaultTempFileManager { - public void _clear() { - super.clear(); - } - - @Override - public void clear() { - // ignore - } + @Before + public void setUp() throws Exception { + this.testServer = new TestServer(); + this.tempFileManager = new TestTempFileManager(); } - public static class TestServer extends NanoHTTPD { - public Response response = new Response(""); - public String uri; - public Method method; - public Map<String, String> header; - public Map<String, String> parms; - public Map<String, String> files; - public Map<String, List<String>> decodedParamters; - public Map<String, List<String>> decodedParamtersFromParameter; - public String queryParameterString; - - public TestServer() { - super(8192); - } - - public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - return new HTTPSession(tempFileManager, inputStream, outputStream); - } - - public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress); - } - - @Override public Response serve(IHTTPSession session) { - this.uri = session.getUri(); - this.method = session.getMethod(); - this.header = session.getHeaders(); - this.parms = session.getParms(); - this.files = new HashMap<String, String>(); - try { - session.parseBody(files); - } catch (Exception e) { - e.printStackTrace(); - } - queryParameterString = session.getQueryParameterString(); - this.decodedParamtersFromParameter = decodeParameters(queryParameterString); - this.decodedParamters = decodeParameters(session.getQueryParameterString()); - return response; - } + @After + public void tearDown() { + this.tempFileManager._clear(); + } - @Override - public String decodePercent(String str) { - return super.decodePercent(str); - } + @Test + public void testServerExists() { + assertNotNull(this.testServer); } } diff --git a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java index 75a0a49..1494e28 100644 --- a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java @@ -1,30 +1,66 @@ package fi.iki.elonen; -import org.junit.Test; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.net.InetAddress; -import static org.junit.Assert.assertEquals; +import org.junit.Ignore; +import org.junit.Test; public class HttpSessionHeadersTest extends HttpServerTest { + private static final String DUMMY_REQUEST_CONTENT = "dummy request content"; - private static final TestTempFileManager TEST_TEMP_FILE_MANAGER = new TestTempFileManager(); - @Override - public void setUp() { - super.setUp(); - } + private static final TestTempFileManager TEST_TEMP_FILE_MANAGER = new TestTempFileManager(); @Test + @Ignore public void testHeadersRemoteIp() throws Exception { - ByteArrayInputStream inputStream = new ByteArrayInputStream(DUMMY_REQUEST_CONTENT.getBytes()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(HttpSessionHeadersTest.DUMMY_REQUEST_CONTENT.getBytes()); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - String[] ipAddresses = { "127.0.0.1", "192.168.1.1", "192.30.252.129" }; - for(String ipAddress : ipAddresses) { + String[] ipAddresses = { + "127.0.0.1", + "192.168.1.1", + "192.30.252.129" + }; + for (String ipAddress : ipAddresses) { InetAddress inetAddress = InetAddress.getByName(ipAddress); - NanoHTTPD.HTTPSession session = testServer.createSession(TEST_TEMP_FILE_MANAGER, inputStream, outputStream, inetAddress); + NanoHTTPD.HTTPSession session = this.testServer.createSession(HttpSessionHeadersTest.TEST_TEMP_FILE_MANAGER, inputStream, outputStream, inetAddress); assertEquals(ipAddress, session.getHeaders().get("remote-addr")); assertEquals(ipAddress, session.getHeaders().get("http-client-ip")); } diff --git a/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java b/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java new file mode 100644 index 0000000..eda60a3 --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java @@ -0,0 +1,79 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; + +import org.junit.Test; + +public class InvalidRequestTest extends HttpServerTest { + + @Test + public void testGetRequestWithoutProtocol() { + invokeServer("GET " + HttpServerTest.URI + "\r\nX-Important-Header: foo"); + + assertNotNull(this.testServer.parms); + assertTrue(this.testServer.header.size() > 0); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); + } + + @Test + public void testGetRequestWithProtocol() { + invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\r\nX-Important-Header: foo"); + + assertNotNull(this.testServer.parms); + assertTrue(this.testServer.header.size() > 0); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); + } + + @Test + public void testPostRequestWithoutProtocol() { + invokeServer("POST " + HttpServerTest.URI + "\r\nContent-Length: 123"); + assertNotNull(this.testServer.parms); + assertTrue(this.testServer.header.size() > 0); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); + } + + @Test + public void testPostRequestWithProtocol() { + invokeServer("POST " + HttpServerTest.URI + " HTTP/1.1\r\nContent-Length: 123"); + assertNotNull(this.testServer.parms); + assertTrue(this.testServer.header.size() > 0); + assertNotNull(this.testServer.files); + assertNotNull(this.testServer.uri); + } +} diff --git a/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java b/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java new file mode 100644 index 0000000..b586bc2 --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java @@ -0,0 +1,87 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import fi.iki.elonen.NanoHTTPD.DefaultTempFile; + +/** + * Created by Victor Nikiforov on 10/16/15. + */ +public class JavaIOTempDirExistTest { + + @Test + public void testJavaIoTempDefault() throws Exception { + String tmpdir = System.getProperty("java.io.tmpdir"); + NanoHTTPD.DefaultTempFileManager manager = new NanoHTTPD.DefaultTempFileManager(); + DefaultTempFile tempFile = (DefaultTempFile) manager.createTempFile("xx"); + File tempFileBackRef = new File(tempFile.getName()); + Assert.assertEquals(tempFileBackRef.getParentFile(), new File(tmpdir)); + + // force an exception + tempFileBackRef.delete(); + Exception e = null; + try { + tempFile.delete(); + } catch (Exception ex) { + e = ex; + } + Assert.assertNotNull(e); + manager.clear(); + } + + @Test + public void testJavaIoTempSpecific() throws IOException { + final String tmpdir = System.getProperty("java.io.tmpdir"); + try { + String tempFileName = UUID.randomUUID().toString(); + File newDir = new File("target", tempFileName); + System.setProperty("java.io.tmpdir", newDir.getAbsolutePath()); + Assert.assertEquals(false, newDir.exists()); + new NanoHTTPD.DefaultTempFileManager(); + Assert.assertEquals(true, newDir.exists()); + newDir.delete(); + } finally { + System.setProperty("java.io.tmpdir", tmpdir); + } + + } + +} diff --git a/core/src/test/java/fi/iki/elonen/MimeTest.java b/core/src/test/java/fi/iki/elonen/MimeTest.java new file mode 100644 index 0000000..046ef00 --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/MimeTest.java @@ -0,0 +1,62 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import org.junit.Assert; +import org.junit.Test; + +public class MimeTest { + + @Test + public void testExistingMimeType() throws Exception { + Assert.assertEquals("text/html", NanoHTTPD.getMimeTypeForFile("xxxx.html")); + } + + @Test + public void testNotExistingMimeType() throws Exception { + Assert.assertNull(NanoHTTPD.mimeTypes().get("notExistent")); + Assert.assertEquals("application/octet-stream", NanoHTTPD.getMimeTypeForFile("xxxx.notExistent")); + } + + @Test + public void testOverwritenMimeType() throws Exception { + Assert.assertEquals("video/wrongOverwrite", NanoHTTPD.getMimeTypeForFile("xxxx.ts")); + } + + @Test + public void testManualMimeType() throws Exception { + NanoHTTPD.mimeTypes().put("flv", "video/manualOverwrite"); + Assert.assertEquals("video/manualOverwrite", NanoHTTPD.getMimeTypeForFile("xxxx.flv")); + } +} diff --git a/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java b/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java new file mode 100644 index 0000000..1722058 --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java @@ -0,0 +1,90 @@ +package fi.iki.elonen; + +import java.io.File; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import fi.iki.elonen.NanoHTTPD.SecureServerSocketFactory; + +public class SSLServerSocketFactoryTest extends HttpServerTest { + + @Test + public void testSSLConnection() throws ClientProtocolException, IOException { + DefaultHttpClient httpclient = new DefaultHttpClient(); + HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html"); + HttpResponse response = httpclient.execute(httphead); + HttpEntity entity = response.getEntity(); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + + Assert.assertEquals(9043, this.testServer.getListeningPort()); + Assert.assertTrue(this.testServer.isAlive()); + } + + @Before + public void setUp() throws Exception { + System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath()); + this.testServer = new TestServer(9043); + this.testServer.setServerSocketFactory(new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null)); + this.tempFileManager = new TestTempFileManager(); + this.testServer.start(); + try { + long start = System.currentTimeMillis(); + Thread.sleep(100L); + while (!this.testServer.wasStarted()) { + Thread.sleep(100L); + if (System.currentTimeMillis() - start > 2000) { + Assert.fail("could not start server"); + } + } + } catch (InterruptedException e) { + } + } + + @After + public void tearDown() { + this.testServer.stop(); + } +} diff --git a/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java b/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java new file mode 100644 index 0000000..17112ef --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java @@ -0,0 +1,102 @@ +package fi.iki.elonen; + +import java.io.File; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.net.ServerSocket; + +import org.junit.Assert; +import org.junit.Test; + +import fi.iki.elonen.HttpServerTest.TestServer; +import fi.iki.elonen.NanoHTTPD.SecureServerSocketFactory; + +public class ServerSocketFactoryTest extends NanoHTTPD { + + public static final int PORT = 8192; + + public ServerSocketFactoryTest() { + super(PORT); + + this.setServerSocketFactory(new TestFactory()); + } + + @Test + public void isCustomServerSocketFactory() { + System.out.println("CustomServerSocketFactory test"); + Assert.assertTrue(this.getServerSocketFactory() instanceof TestFactory); + } + + @Test + public void testCreateServerSocket() { + System.out.println("CreateServerSocket test"); + ServerSocket ss = null; + try { + ss = this.getServerSocketFactory().create(); + } catch (IOException e) { + } + Assert.assertTrue(ss != null); + } + + @Test + public void testSSLServerSocketFail() { + String[] protocols = { + "" + }; + System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath()); + ServerSocketFactory ssFactory = new SecureServerSocketFactory(null, protocols); + ServerSocket ss = null; + try { + ss = ssFactory.create(); + } catch (Exception e) { + } + Assert.assertTrue(ss == null); + + } + + private class TestFactory implements ServerSocketFactory { + + @Override + public ServerSocket create() { + try { + return new ServerSocket(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + } +} diff --git a/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java index 0d54b37..5cf5719 100644 --- a/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java +++ b/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java @@ -1,6 +1,45 @@ package fi.iki.elonen.integration; -import fi.iki.elonen.NanoHTTPD; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + import org.apache.http.client.CookieStore; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; @@ -8,78 +47,77 @@ import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.cookie.BasicClientCookie; import org.junit.Test; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; - -import static org.junit.Assert.*; +import fi.iki.elonen.NanoHTTPD; /** - * @author Paul S. Hawke (paul.hawke@gmail.com) - * On: 9/2/13 at 10:10 PM + * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/2/13 at 10:10 PM */ public class CookieIntegrationTest extends IntegrationTestBase<CookieIntegrationTest.CookieTestServer> { - @Test - public void testNoCookies() throws Exception { - HttpGet httpget = new HttpGet("http://localhost:8192/"); - ResponseHandler<String> responseHandler = new BasicResponseHandler(); - httpclient.execute(httpget, responseHandler); + public static class CookieTestServer extends NanoHTTPD { - CookieStore cookies = httpclient.getCookieStore(); - assertEquals(0, cookies.getCookies().size()); + List<Cookie> cookiesReceived = new ArrayList<Cookie>(); + + List<Cookie> cookiesToSend = new ArrayList<Cookie>(); + + public CookieTestServer() { + super(8192); + } + + @Override + public Response serve(IHTTPSession session) { + CookieHandler cookies = session.getCookies(); + for (String cookieName : cookies) { + this.cookiesReceived.add(new Cookie(cookieName, cookies.read(cookieName))); + } + for (Cookie c : this.cookiesToSend) { + cookies.set(c); + } + return newFixedLengthResponse("Cookies!"); + } + } + + @Override + public CookieTestServer createTestServer() { + return new CookieTestServer(); } @Test public void testCookieSentBackToClient() throws Exception { - testServer.cookiesToSend.add(new NanoHTTPD.Cookie("name", "value", 30)); + this.testServer.cookiesToSend.add(new NanoHTTPD.Cookie("name", "value", 30)); HttpGet httpget = new HttpGet("http://localhost:8192/"); ResponseHandler<String> responseHandler = new BasicResponseHandler(); - httpclient.execute(httpget, responseHandler); + this.httpclient.execute(httpget, responseHandler); - CookieStore cookies = httpclient.getCookieStore(); + CookieStore cookies = this.httpclient.getCookieStore(); assertEquals(1, cookies.getCookies().size()); assertEquals("name", cookies.getCookies().get(0).getName()); assertEquals("value", cookies.getCookies().get(0).getValue()); } @Test + public void testNoCookies() throws Exception { + HttpGet httpget = new HttpGet("http://localhost:8192/"); + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + this.httpclient.execute(httpget, responseHandler); + + CookieStore cookies = this.httpclient.getCookieStore(); + assertEquals(0, cookies.getCookies().size()); + } + + @Test public void testServerReceivesCookiesSentFromClient() throws Exception { BasicClientCookie clientCookie = new BasicClientCookie("name", "value"); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, 100); clientCookie.setExpiryDate(calendar.getTime()); clientCookie.setDomain("localhost"); - httpclient.getCookieStore().addCookie(clientCookie); + this.httpclient.getCookieStore().addCookie(clientCookie); HttpGet httpget = new HttpGet("http://localhost:8192/"); ResponseHandler<String> responseHandler = new BasicResponseHandler(); - httpclient.execute(httpget, responseHandler); + this.httpclient.execute(httpget, responseHandler); - assertEquals(1, testServer.cookiesReceived.size()); - assertTrue(testServer.cookiesReceived.get(0).getHTTPHeader().contains("name=value")); - } - - @Override public CookieTestServer createTestServer() { - return new CookieTestServer(); - } - - public static class CookieTestServer extends NanoHTTPD { - List<Cookie> cookiesReceived = new ArrayList<Cookie>(); - List<Cookie> cookiesToSend = new ArrayList<Cookie>(); - - public CookieTestServer() { - super(8192); - } - - @Override public Response serve(IHTTPSession session) { - CookieHandler cookies = session.getCookies(); - for (String cookieName : cookies) { - cookiesReceived.add(new Cookie(cookieName, cookies.read(cookieName))); - } - for (Cookie c : cookiesToSend) { - cookies.set(c); - } - return new Response("Cookies!"); - } + assertEquals(1, this.testServer.cookiesReceived.size()); + assertTrue(this.testServer.cookiesReceived.get(0).getHTTPHeader().contains("name=value")); } } diff --git a/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java new file mode 100644 index 0000000..a278406 --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java @@ -0,0 +1,168 @@ +package fi.iki.elonen.integration; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DecompressingHttpClient; +import org.apache.http.util.EntityUtils; +import org.junit.Test; + +import fi.iki.elonen.NanoHTTPD; + +public class GZipIntegrationTest extends IntegrationTestBase<GZipIntegrationTest.TestServer> { + + public static class TestServer extends NanoHTTPD { + + public Response response; + + public TestServer() { + super(8192); + } + + @Override + public Response serve(IHTTPSession session) { + return response; + } + + @Override + protected boolean useGzipWhenAccepted(Response r) { + return true; + } + } + + @Override + public TestServer createTestServer() { + return new TestServer(); + } + + @Test + public void contentEncodingShouldBeAddedToFixedLengthResponses() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = httpclient.execute(request); + Header contentEncoding = response.getFirstHeader("content-encoding"); + assertNotNull("Content-Encoding should be set", contentEncoding); + assertEquals("gzip", contentEncoding.getValue()); + } + + @Test + public void contentEncodingShouldBeAddedToChunkedResponses() throws IOException { + InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8")); + testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = httpclient.execute(request); + Header contentEncoding = response.getFirstHeader("content-encoding"); + assertNotNull("Content-Encoding should be set", contentEncoding); + assertEquals("gzip", contentEncoding.getValue()); + } + + @Test + public void shouldFindCorrectAcceptEncodingAmongMany() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "deflate,gzip"); + HttpResponse response = httpclient.execute(request); + Header contentEncoding = response.getFirstHeader("content-encoding"); + assertNotNull("Content-Encoding should be set", contentEncoding); + assertEquals("gzip", contentEncoding.getValue()); + } + + @Test + public void contentLengthShouldBeRemovedFromZippedResponses() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = httpclient.execute(request); + Header contentLength = response.getFirstHeader("content-length"); + assertNull("Content-Length should not be set when gzipping response", contentLength); + } + + @Test + public void fixedLengthContentIsEncodedProperly() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = new DecompressingHttpClient(httpclient).execute(request); + assertEquals("This is a test", EntityUtils.toString(response.getEntity())); + } + + @Test + public void chunkedContentIsEncodedProperly() throws IOException { + InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8")); + testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = new DecompressingHttpClient(httpclient).execute(request); + assertEquals("This is a test", EntityUtils.toString(response.getEntity())); + } + + @Test + public void noGzipWithoutAcceptEncoding() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + HttpGet request = new HttpGet("http://localhost:8192/"); + HttpResponse response = httpclient.execute(request); + Header contentEncoding = response.getFirstHeader("content-encoding"); + assertThat(contentEncoding, is(nullValue())); + assertEquals("This is a test", EntityUtils.toString(response.getEntity())); + } + + @Test + public void contentShouldNotBeGzippedIfContentLengthIsAddedManually() throws IOException { + testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test"); + testServer.response.addHeader("Content-Length", "" + ("This is a test".getBytes("UTF-8").length)); + HttpGet request = new HttpGet("http://localhost:8192/"); + request.addHeader("Accept-encoding", "gzip"); + HttpResponse response = httpclient.execute(request); + Header contentEncoding = response.getFirstHeader("content-encoding"); + assertNull("Content-Encoding should not be set when manually setting content-length", contentEncoding); + assertEquals("This is a test", EntityUtils.toString(response.getEntity())); + + } + +} diff --git a/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java index bc0a9d9..eef2f22 100644 --- a/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java +++ b/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java @@ -1,8 +1,49 @@ package fi.iki.elonen.integration; -import fi.iki.elonen.NanoHTTPD; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; @@ -11,62 +52,66 @@ import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.impl.client.BasicResponseHandler; -import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; -import org.junit.After; -import org.junit.Before; +import org.apache.http.util.EntityUtils; import org.junit.Test; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.Response.Status; /** - * @author Paul S. Hawke (paul.hawke@gmail.com) - * On: 5/19/13 at 5:36 PM + * @author Paul S. Hawke (paul.hawke@gmail.com) On: 5/19/13 at 5:36 PM */ public class GetAndPostIntegrationTest extends IntegrationTestBase<GetAndPostIntegrationTest.TestServer> { - @Test - public void testSimpleGetRequest() throws Exception { - testServer.response = "testSimpleGetRequest"; + public static class TestServer extends NanoHTTPD { - HttpGet httpget = new HttpGet("http://localhost:8192/"); - ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httpget, responseHandler); + public String response; - assertEquals("GET:testSimpleGetRequest", responseBody); - } + public TestServer() { + super(8192); + } - @Test - public void testGetRequestWithParameters() throws Exception { - testServer.response = "testGetRequestWithParameters"; + @Override + public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) { + StringBuilder sb = new StringBuilder(String.valueOf(method) + ':' + this.response); - HttpGet httpget = new HttpGet("http://localhost:8192/?age=120&gender=Male"); - ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httpget, responseHandler); + if (parms.size() > 1) { + parms.remove("NanoHttpd.QUERY_STRING"); + sb.append("-params=").append(parms.size()); + List<String> p = new ArrayList<String>(parms.keySet()); + Collections.sort(p); + for (String k : p) { + sb.append(';').append(k).append('=').append(parms.get(k)); + } + } + if ("/chin".equals(uri)) { + return newFixedLengthResponse(Status.OK, "application/octet-stream", sb.toString()); + } else { + return newFixedLengthResponse(sb.toString()); + } + } + } - assertEquals("GET:testGetRequestWithParameters-params=2;age=120;gender=Male", responseBody); + @Override + public TestServer createTestServer() { + return new TestServer(); } @Test - public void testPostWithNoParameters() throws Exception { - testServer.response = "testPostWithNoParameters"; + public void testGetRequestWithParameters() throws Exception { + this.testServer.response = "testGetRequestWithParameters"; - HttpPost httppost = new HttpPost("http://localhost:8192/"); + HttpGet httpget = new HttpGet("http://localhost:8192/?age=120&gender=Male"); ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httppost, responseHandler); + String responseBody = this.httpclient.execute(httpget, responseHandler); - assertEquals("POST:testPostWithNoParameters", responseBody); + assertEquals("GET:testGetRequestWithParameters-params=2;age=120;gender=Male", responseBody); } @Test public void testPostRequestWithFormEncodedParameters() throws Exception { - testServer.response = "testPostRequestWithFormEncodedParameters"; + this.testServer.response = "testPostRequestWithFormEncodedParameters"; HttpPost httppost = new HttpPost("http://localhost:8192/"); List<NameValuePair> postParameters = new ArrayList<NameValuePair>(); @@ -75,14 +120,14 @@ public class GetAndPostIntegrationTest extends IntegrationTestBase<GetAndPostInt httppost.setEntity(new UrlEncodedFormEntity(postParameters)); ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httppost, responseHandler); + String responseBody = this.httpclient.execute(httppost, responseHandler); assertEquals("POST:testPostRequestWithFormEncodedParameters-params=2;age=120;gender=Male", responseBody); } @Test public void testPostRequestWithMultipartEncodedParameters() throws Exception { - testServer.response = "testPostRequestWithMultipartEncodedParameters"; + this.testServer.response = "testPostRequestWithMultipartEncodedParameters"; HttpPost httppost = new HttpPost("http://localhost:8192/"); MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); @@ -91,37 +136,53 @@ public class GetAndPostIntegrationTest extends IntegrationTestBase<GetAndPostInt httppost.setEntity(reqEntity); ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httppost, responseHandler); + String responseBody = this.httpclient.execute(httppost, responseHandler); assertEquals("POST:testPostRequestWithMultipartEncodedParameters-params=2;age=120;gender=Male", responseBody); } - @Override public TestServer createTestServer() { - return new TestServer(); + @Test + public void testPostWithNoParameters() throws Exception { + this.testServer.response = "testPostWithNoParameters"; + + HttpPost httppost = new HttpPost("http://localhost:8192/"); + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + String responseBody = this.httpclient.execute(httppost, responseHandler); + + assertEquals("POST:testPostWithNoParameters", responseBody); } - public static class TestServer extends NanoHTTPD { - public String response; + @Test + public void testSimpleGetRequest() throws Exception { + this.testServer.response = "testSimpleGetRequest"; - public TestServer() { - super(8192); - } + HttpGet httpget = new HttpGet("http://localhost:8192/"); + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + String responseBody = this.httpclient.execute(httpget, responseHandler); - @Override - public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) { - StringBuilder sb = new StringBuilder(String.valueOf(method) + ':' + response); + assertEquals("GET:testSimpleGetRequest", responseBody); + } - if (parms.size() > 1) { - parms.remove("NanoHttpd.QUERY_STRING"); - sb.append("-params=").append(parms.size()); - List<String> p = new ArrayList<String>(parms.keySet()); - Collections.sort(p); - for (String k : p) { - sb.append(';').append(k).append('=').append(parms.get(k)); - } + @Test + public void testPostRequestWithMultipartExtremEncodedParameters() throws Exception { + this.testServer.response = "testPostRequestWithMultipartEncodedParameters"; + + HttpPost httppost = new HttpPost("http://localhost:8192/chin"); + MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, "sfsadfasdf", Charset.forName("UTF-8")); + reqEntity.addPart("specialString", new StringBody("拖拉图片到浏览器,可以实现预览功能", "text/plain", Charset.forName("UTF-8"))); + reqEntity.addPart("gender", new StringBody("图片名称", Charset.forName("UTF-8")) { + + @Override + public String getFilename() { + return "图片名称"; } + }); + httppost.setEntity(reqEntity); + HttpResponse response = this.httpclient.execute(httppost); - return new Response(sb.toString()); - } + HttpEntity entity = response.getEntity(); + String responseBody = EntityUtils.toString(entity, "UTF-8"); + + assertEquals("POST:testPostRequestWithMultipartEncodedParameters-params=2;gender=图片名称;specialString=拖拉图片到浏览器,可以实现预览功能", responseBody); } } diff --git a/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java b/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java index eb34033..fd3d6d3 100644 --- a/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java +++ b/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java @@ -1,27 +1,63 @@ package fi.iki.elonen.integration; -import fi.iki.elonen.NanoHTTPD; -import org.apache.http.client.HttpClient; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; + import org.apache.http.impl.client.DefaultHttpClient; import org.junit.After; import org.junit.Before; -import java.io.IOException; +import fi.iki.elonen.NanoHTTPD; /** - * @author Paul S. Hawke (paul.hawke@gmail.com) - * On: 9/2/13 at 10:02 PM + * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/2/13 at 10:02 PM */ public abstract class IntegrationTestBase<T extends NanoHTTPD> { + protected DefaultHttpClient httpclient; + protected T testServer; + public abstract T createTestServer(); + @Before public void setUp() { - testServer = createTestServer(); - httpclient = new DefaultHttpClient(); + this.testServer = createTestServer(); + this.httpclient = new DefaultHttpClient(); try { - testServer.start(); + this.testServer.start(); } catch (IOException e) { e.printStackTrace(); } @@ -29,9 +65,7 @@ public abstract class IntegrationTestBase<T extends NanoHTTPD> { @After public void tearDown() { - httpclient.getConnectionManager().shutdown(); - testServer.stop(); + this.httpclient.getConnectionManager().shutdown(); + this.testServer.stop(); } - - public abstract T createTestServer(); } diff --git a/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java index 1e260b2..41b84fd 100644 --- a/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java +++ b/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java @@ -1,5 +1,38 @@ package fi.iki.elonen.integration; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + import static org.junit.Assert.assertEquals; import java.io.DataInputStream; @@ -16,34 +49,13 @@ import fi.iki.elonen.NanoHTTPD; public class PutStreamIntegrationTest extends IntegrationTestBase<PutStreamIntegrationTest.TestServer> { - @Test - public void testSimplePutRequest() throws Exception { - String expected = "This HttpPut request has a content-length of 48."; - - HttpPut httpput = new HttpPut("http://localhost:8192/"); - httpput.setEntity(new ByteArrayEntity(expected.getBytes())); - ResponseHandler<String> responseHandler = new BasicResponseHandler(); - String responseBody = httpclient.execute(httpput, responseHandler); - - assertEquals("PUT:" + expected, responseBody); - } - - @Override public TestServer createTestServer() { - return new TestServer(); - } - public static class TestServer extends NanoHTTPD { + public TestServer() { super(8192); } @Override - public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) - { - throw new UnsupportedOperationException(); - } - - @Override public Response serve(IHTTPSession session) { Method method = session.getMethod(); Map<String, String> headers = session.getHeaders(); @@ -54,13 +66,34 @@ public class PutStreamIntegrationTest extends IntegrationTestBase<PutStreamInteg DataInputStream dataInputStream = new DataInputStream(session.getInputStream()); body = new byte[contentLength]; dataInputStream.readFully(body, 0, contentLength); - } - catch(IOException e) { - return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, e.getMessage()); + } catch (IOException e) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, e.getMessage()); } String response = String.valueOf(method) + ':' + new String(body); - return new Response(response); + return newFixedLengthResponse(response); + } + + @Override + public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { + throw new UnsupportedOperationException(); } } + + @Override + public TestServer createTestServer() { + return new TestServer(); + } + + @Test + public void testSimplePutRequest() throws Exception { + String expected = "This HttpPut request has a content-length of 48."; + + HttpPut httpput = new HttpPut("http://localhost:8192/"); + httpput.setEntity(new ByteArrayEntity(expected.getBytes())); + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + String responseBody = this.httpclient.execute(httpput, responseHandler); + + assertEquals("PUT:" + expected, responseBody); + } } diff --git a/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java b/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java index 0fcb275..cd305ea 100644 --- a/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java +++ b/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java @@ -1,9 +1,39 @@ package fi.iki.elonen.integration; -import static org.junit.Assert.*; -import fi.iki.elonen.NanoHTTPD; +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ -import org.junit.Test; +import static org.junit.Assert.fail; import java.io.IOException; import java.io.InputStream; @@ -11,8 +41,24 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import org.junit.Test; + +import fi.iki.elonen.NanoHTTPD; + public class ShutdownTest { + private class TestServer extends NanoHTTPD { + + public TestServer() { + super(8092); + } + + @Override + public Response serve(IHTTPSession session) { + return newFixedLengthResponse("Whatever"); + } + } + @Test public void connectionsAreClosedWhenServerStops() throws IOException { TestServer server = new TestServer(); @@ -38,16 +84,4 @@ public class ShutdownTest { in.close(); } - private class TestServer extends NanoHTTPD { - - public TestServer() { - super(8092); - } - - @Override - public Response serve(IHTTPSession session) { - return new Response("Whatever"); - } - } - } diff --git a/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties b/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties new file mode 100644 index 0000000..2f353d8 --- /dev/null +++ b/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties @@ -0,0 +1,3 @@ +#test mime types for nanohttpd +blabla=text/blabla +ts=video/wrongOverwrite
\ No newline at end of file diff --git a/core/src/test/resources/file-upload-test.htm b/core/src/test/resources/file-upload-test.htm index 7d553bf..e64c516 100644 --- a/core/src/test/resources/file-upload-test.htm +++ b/core/src/test/resources/file-upload-test.htm @@ -1,3 +1,35 @@ +<!-- + #%L + NanoHttpd-Core + %% + Copyright (C) 2012 - 2015 nanohttpd + %% + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the nanohttpd nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + #L% + --> <html> <body> <p>This is a file upload test for NanoHTTPD.</p> diff --git a/core/src/test/resources/keystore.jks b/core/src/test/resources/keystore.jks Binary files differnew file mode 100644 index 0000000..354b5d3 --- /dev/null +++ b/core/src/test/resources/keystore.jks diff --git a/core/src/test/resources/multipart-form-test.htm b/core/src/test/resources/multipart-form-test.htm index eab4dc7..8504475 100644 --- a/core/src/test/resources/multipart-form-test.htm +++ b/core/src/test/resources/multipart-form-test.htm @@ -1,3 +1,35 @@ +<!-- + #%L + NanoHttpd-Core + %% + Copyright (C) 2012 - 2015 nanohttpd + %% + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the nanohttpd nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + #L% + --> <html> <body> <p>This is a multipart-form test for NanoHTTPD.</p> |