summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/fi/iki/elonen/NanoHTTPD.java1200
1 files changed, 1200 insertions, 0 deletions
diff --git a/src/main/java/fi/iki/elonen/NanoHTTPD.java b/src/main/java/fi/iki/elonen/NanoHTTPD.java
new file mode 100644
index 0000000..1c1abc6
--- /dev/null
+++ b/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -0,0 +1,1200 @@
+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.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PushbackInputStream;
+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.URLDecoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.openslx.util.PrioThreadFactory;
+
+/**
+ * A simple, tiny, nicely embeddable HTTP server in Java
+ * <p/>
+ * <p/>
+ * NanoHTTPD
+ * <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>
+ * <ul>
+ * <p/>
+ * <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>Supports both dynamic content and file serving</li>
+ * <li>Supports file upload (since version 1.2, 2010)</li>
+ * <li>Supports partial content (streaming)</li>
+ * <li>Supports ETags</li>
+ * <li>Never caches anything</li>
+ * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
+ * <li>Default code serves files and shows all HTTP parameters and headers</li>
+ * <li>File server supports directory listing, index.html and index.htm</li>
+ * <li>File server supports partial content (streaming)</li>
+ * <li>File server supports ETags</li>
+ * <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 to lower case so they don't vary between
+ * browsers/clients</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * <p/>
+ * <b>How to use: </b>
+ * <ul>
+ * <p/>
+ * <li>Subclass and implement serve() and embed to your own program</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD
+ * licence)
+ */
+public abstract class NanoHTTPD implements Runnable
+{
+
+ /**
+ * 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 = 10000;
+ /**
+ * 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>();
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ private AsyncRunner asyncRunner;
+
+ protected int maxRequestSize = 0;
+
+ /**
+ * Constructs an HTTP server on given port.
+ */
+ public NanoHTTPD( int port )
+ {
+ this( null, port );
+ }
+
+ /**
+ * Constructs an HTTP server on given hostname and port.
+ */
+ public NanoHTTPD( String hostname, int port )
+ {
+ this.hostname = hostname;
+ this.myPort = port;
+ setAsyncRunner( new DefaultAsyncRunner() );
+ }
+
+ protected static final void safeClose( Closeable closeable )
+ {
+ if ( closeable != null ) {
+ try {
+ closeable.close();
+ } catch ( IOException e ) {
+ }
+ }
+ }
+
+ /**
+ * Start the server.
+ *
+ * @throws IOException if the socket is in use.
+ */
+ @Override
+ public void run()
+ {
+ try {
+ myServerSocket = new ServerSocket();
+ myServerSocket.setReuseAddress( true );
+ myServerSocket.bind( ( hostname != null ) ? new InetSocketAddress( hostname, myPort )
+ : new InetSocketAddress( myPort ) );
+ } catch ( Exception e ) {
+ throw new RuntimeException( e );
+ }
+
+ 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();
+ HTTPSession session = new HTTPSession( inputStream, outputStream,
+ finalAccept.getInetAddress() );
+ while ( !finalAccept.isClosed() && !finalAccept.isInputShutdown() ) {
+ 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 SocketTimeoutException )
+ && ! ( 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() );
+ serverStopped();
+ }
+
+ public void serverStopped()
+ {
+
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop()
+ {
+ try {
+ safeClose( myServerSocket );
+ closeAllConnections();
+ } catch ( Exception e ) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 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 );
+ }
+
+ /**
+ * Forcibly closes all connections that are open.
+ */
+ public synchronized void closeAllConnections()
+ {
+ for ( Socket socket : openConnections ) {
+ safeClose( socket );
+ }
+ }
+
+ public final int getListeningPort()
+ {
+ return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
+ }
+
+ public final boolean wasStarted()
+ {
+ return myServerSocket != null;
+ }
+
+ public final boolean isAlive()
+ {
+ return wasStarted() && !myServerSocket.isClosed();
+ }
+
+ /**
+ * 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 new Response( Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found" );
+ }
+
+ /**
+ * 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 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() );
+ }
+ }
+
+ Map<String, String> parms = session.getParms();
+ parms.put( QUERY_STRING_PARAMETER, session.getQueryParameterString() );
+ return serve( session.getUri(), method, session.getHeaders(), parms, files );
+ }
+
+ /**
+ * 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 ) {
+ }
+ 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&lt;String&gt;</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 ) );
+ }
+
+ /**
+ * 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&lt;String&gt;</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 );
+ }
+ }
+ }
+ return parms;
+ }
+
+ // ------------------------------------------------------------------------------- //
+ //
+ // Threading Strategy.
+ //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ *
+ * @param asyncRunner new strategy for handling threads.
+ */
+ public void setAsyncRunner( AsyncRunner asyncRunner )
+ {
+ this.asyncRunner = asyncRunner;
+ }
+
+ /**
+ * 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;
+
+ static Method lookup( String method )
+ {
+ for ( Method m : Method.values() ) {
+ if ( m.toString().equalsIgnoreCase( method ) ) {
+ return m;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ public interface AsyncRunner
+ {
+ void exec( Runnable code );
+ }
+
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Default threading strategy for NanoHTTPD.
+ * <p/>
+ * <p>
+ * Uses a thread pool.
+ * </p>
+ */
+ public static class DefaultAsyncRunner implements AsyncRunner
+ {
+ private ExecutorService pool = new ThreadPoolExecutor( 2, 24, 1, TimeUnit.MINUTES,
+ new ArrayBlockingQueue<Runnable>( 16 ), new PrioThreadFactory( "httpd", Thread.NORM_PRIORITY ) );
+
+ @Override
+ public void exec( Runnable code )
+ {
+ try {
+ pool.execute( code );
+ } catch ( RejectedExecutionException e ) {
+ }
+ }
+ }
+
+ /**
+ * HTTP response. Return one of these from serve().
+ */
+ public static class Response
+ {
+ /**
+ * HTTP status code after processing, e.g. "200 OK", Status.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 final Map<String, String> header = new HashMap<String, String>();
+ /**
+ * The request method that spawned this response.
+ */
+ private Method requestMethod;
+ /**
+ * Use chunkedTransfer
+ */
+ private boolean chunkedTransfer;
+
+ /**
+ * Default constructor: response = Status.OK, mime = MIME_HTML and your
+ * supplied message
+ */
+ public Response( String msg )
+ {
+ this( Status.OK, MIME_HTML, msg );
+ }
+
+ /**
+ * Basic constructor.
+ */
+ public Response( IStatus status, String mimeType, InputStream data, boolean chunked )
+ {
+ this.status = status;
+ this.mimeType = mimeType;
+ this.data = data;
+ this.chunkedTransfer = chunked;
+ }
+
+ /**
+ * Basic constructor. Enable chunked transfer for everything
+ * except ByteArrayInputStream.
+ */
+ public Response( IStatus status, String mimeType, InputStream data )
+ {
+ this( status, mimeType, data, ! ( data instanceof ByteArrayInputStream ) );
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of given byte array.
+ */
+ public Response( IStatus status, String mimeType, byte[] data )
+ {
+ this( status, mimeType, data == null ? null : new ByteArrayInputStream( data ) );
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of given text.
+ */
+ public Response( IStatus status, String mimeType, String txt )
+ {
+ this( status, mimeType, txt == null ? null : txt.getBytes( StandardCharsets.UTF_8 ) );
+ }
+
+ /**
+ * Adds given line to the header.
+ */
+ public void addHeader( String name, String value )
+ {
+ header.put( name, value );
+ }
+
+ public String getHeader( String name )
+ {
+ return header.get( name );
+ }
+
+ private static final ThreadLocal<SimpleDateFormat> headerDateFormatter = new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ protected SimpleDateFormat initialValue()
+ {
+ return new SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'" );
+ }
+ };
+
+ /**
+ * Sends given response to the socket.
+ */
+ protected void send( OutputStream outputStream ) throws IOException
+ {
+ String mime = mimeType;
+
+ final StringBuilder sb = new StringBuilder();
+ if ( status == null ) {
+ throw new Error( "sendResponse(): Status can't be null." );
+ }
+ sb.append( "HTTP/1.1 " );
+ sb.append( status.getDescription() );
+ sb.append( " \r\n" );
+
+ if ( mime != null ) {
+ sb.append( "Content-Type: " );
+ sb.append( mime );
+ sb.append( "\r\n" );
+ }
+
+ if ( header.get( "Date" ) == null ) {
+ sb.append( "Date: " );
+ sb.append( headerDateFormatter.get().format( System.currentTimeMillis() ) );
+ sb.append( "\r\n" );
+ }
+
+ for ( Entry<String, String> item : header.entrySet() ) {
+ sb.append( item.getKey() );
+ sb.append( ": " );
+ sb.append( item.getValue() );
+ sb.append( "\r\n" );
+ }
+
+ sendConnectionHeaderIfNotAlreadyPresent( sb, header );
+
+ if ( requestMethod != Method.HEAD && chunkedTransfer ) {
+ sendAsChunked( outputStream, sb );
+ } else {
+ int pending = data != null ? data.available() : 0;
+ pending = sendContentLengthHeaderIfNotAlreadyPresent( sb, header, pending );
+ sb.append( "\r\n" );
+ outputStream.write( sb.toString().getBytes( StandardCharsets.UTF_8 ) );
+ sb.setLength( 0 );
+ sendAsFixedLength( outputStream, pending );
+ }
+
+ if ( sb.length() != 0 ) {
+ outputStream.write( sb.toString().getBytes( StandardCharsets.UTF_8 ) );
+ }
+ safeClose( data );
+ }
+
+ protected int sendContentLengthHeaderIfNotAlreadyPresent( StringBuilder sb,
+ Map<String, String> header, int size )
+ {
+ for ( String headerName : header.keySet() ) {
+ if ( headerName.equalsIgnoreCase( "content-length" ) ) {
+ try {
+ return Integer.parseInt( header.get( headerName ) );
+ } catch ( NumberFormatException ex ) {
+ return size;
+ }
+ }
+ }
+
+ sb.append( "Content-Length: " );
+ sb.append( size );
+ sb.append( "\r\n" );
+ return size;
+ }
+
+ protected void sendConnectionHeaderIfNotAlreadyPresent( StringBuilder sb, Map<String, String> header )
+ {
+ if ( !headerAlreadySent( header, "connection" ) ) {
+ sb.append( "Connection: keep-alive\r\n" );
+ }
+ if ( !headerAlreadySent( header, "keep-alive" ) ) {
+ sb.append( "Keep-Alive: timeout=" );
+ sb.append( SOCKET_READ_TIMEOUT / 1000 - 1 );
+ sb.append( "\r\n" );
+ }
+ }
+
+ private boolean headerAlreadySent( Map<String, String> header, String name )
+ {
+ for ( String headerName : header.keySet() ) {
+ if ( headerName.equalsIgnoreCase( name ) )
+ return true;
+ }
+ return false;
+ }
+
+ private static final byte[] CRLF = "\r\n".getBytes();
+ private static final byte[] CHUNKED_END = "0\r\n\r\n".getBytes();
+ private static final int BUFFER_SIZE = 256 * 1024;
+
+ private void sendAsChunked( OutputStream outputStream, StringBuilder sb ) throws IOException
+ {
+ sb.append( "Transfer-Encoding: chunked\r\n" );
+ sb.append( "\r\n" );
+ outputStream.write( sb.toString().getBytes( StandardCharsets.UTF_8 ) );
+ sb.setLength( 0 );
+ 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( CHUNKED_END );
+ }
+
+ 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;
+ }
+ }
+ }
+
+ public IStatus getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus( IStatus status )
+ {
+ this.status = status;
+ }
+
+ public String getMimeType()
+ {
+ return mimeType;
+ }
+
+ public void setMimeType( String mimeType )
+ {
+ this.mimeType = mimeType;
+ }
+
+ public InputStream getData()
+ {
+ return data;
+ }
+
+ public void setData( InputStream data )
+ {
+ this.data = data;
+ }
+
+ public Method getRequestMethod()
+ {
+ return requestMethod;
+ }
+
+ public void setRequestMethod( Method requestMethod )
+ {
+ this.requestMethod = requestMethod;
+ }
+
+ public void setChunkedTransfer( boolean chunkedTransfer )
+ {
+ this.chunkedTransfer = chunkedTransfer;
+ }
+
+ public interface IStatus
+ {
+ int getRequestStatus();
+
+ String getDescription();
+ }
+
+ /**
+ * Some HTTP response status codes
+ */
+ 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" ),
+ 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" ),
+ RANGE_NOT_SATISFIABLE( 416, "Requested Range Not Satisfiable" ),
+ INTERNAL_ERROR( 500, "Internal Server Error" );
+
+ private final int requestStatus;
+ private final String description;
+
+ Status( int requestStatus, String description )
+ {
+ this.requestStatus = requestStatus;
+ this.description = description;
+ }
+
+ @Override
+ public int getRequestStatus()
+ {
+ return this.requestStatus;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return "" + this.requestStatus + " " + description;
+ }
+ }
+ }
+
+ 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 status;
+ }
+ }
+
+ /**
+ * Handles one session, i.e. parses the HTTP request and returns the
+ * response.
+ */
+ public interface IHTTPSession
+ {
+ void execute() throws IOException;
+
+ Map<String, String> getParms();
+
+ Map<String, String> getHeaders();
+
+ /**
+ * @return the path part of the URL.
+ */
+ String getUri();
+
+ String getQueryParameterString();
+
+ Method getMethod();
+
+ InputStream getInputStream();
+
+ /**
+ * 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;
+ }
+
+ protected class HTTPSession implements IHTTPSession
+ {
+ public static final int BUFSIZE = 8192;
+ 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 String queryParameterString;
+ private String remoteIp;
+
+ public HTTPSession( InputStream inputStream, OutputStream outputStream )
+ {
+ this.inputStream = new PushbackInputStream( inputStream, BUFSIZE );
+ this.outputStream = outputStream;
+ }
+
+ public HTTPSession( InputStream inputStream, OutputStream outputStream, InetAddress inetAddress )
+ {
+ this.inputStream = new PushbackInputStream( inputStream, BUFSIZE );
+ this.outputStream = outputStream;
+ remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1"
+ : inetAddress.getHostAddress().toString();
+ headers = new HashMap<String, String>();
+ }
+
+ @Override
+ public void execute() throws IOException
+ {
+ 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 ) {
+ throw e;
+ }
+ if ( read == -1 ) {
+ // socket was been closed
+ 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 );
+ if ( maxRequestSize != 0 && rlen > maxRequestSize )
+ throw new SocketException( "Request too large" );
+ }
+ if ( splitbyte == 0 ) {
+ throw new SocketException( "Connection closed" );
+ }
+ }
+
+ if ( splitbyte < rlen ) {
+ inputStream.unread( buf, splitbyte, rlen - splitbyte );
+ }
+
+ parms = new HashMap<String, String>();
+ if ( null == headers ) {
+ headers = new HashMap<String, String>();
+ } else {
+ headers.clear();
+ }
+
+ if ( null != remoteIp ) {
+ headers.put( "remote-addr", remoteIp );
+ headers.put( "http-client-ip", remoteIp );
+ }
+
+ // Create a BufferedReader for parsing the header.
+ BufferedReader hin = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( buf,
+ 0, rlen ) ) );
+
+ // Decode the header into parms and header java properties
+ Map<String, String> pre = new HashMap<String, String>();
+ decodeHeader( hin, pre, parms, headers );
+
+ method = Method.lookup( pre.get( "method" ) );
+ if ( method == null ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error." );
+ }
+
+ uri = pre.get( "uri" );
+
+ // Ok, now do the serve()
+ Response r = serve( this );
+ if ( r == null ) {
+ throw new ResponseException( Response.Status.INTERNAL_ERROR,
+ "SERVER INTERNAL ERROR: Serve() returned a null response." );
+ } else {
+ r.setRequestMethod( method );
+ r.send( outputStream );
+ }
+ } catch ( SocketException e ) {
+ // throw it out to close socket object (finalAccept)
+ throw e;
+ } catch ( SocketTimeoutException ste ) {
+ 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 );
+ } catch ( ResponseException re ) {
+ Response r = new Response( re.getStatus(), MIME_PLAINTEXT, re.getMessage() );
+ r.send( outputStream );
+ safeClose( outputStream );
+ }
+ }
+
+ @Override
+ public void parseBody( Map<String, String> files ) throws IOException, ResponseException
+ {
+ long size;
+ if ( headers.containsKey( "content-length" ) ) {
+ size = Integer.parseInt( headers.get( "content-length" ) );
+ } else if ( splitbyte < rlen ) {
+ size = rlen - splitbyte;
+ } else {
+ size = 0;
+ }
+
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if ( Method.POST.equals( method ) ) {
+ String contentType = null;
+ String contentEncoding = null;
+ String contentTypeHeader = headers.get( "content-type" );
+
+ StringTokenizer st = null;
+ if ( contentTypeHeader != null ) {
+ st = new StringTokenizer( contentTypeHeader, "," );
+ if ( st.hasMoreTokens() ) {
+ String part[] = st.nextToken().split( ";\\s*", 2 );
+ contentType = part[0];
+ if ( part.length == 2 ) {
+ contentEncoding = part[1];
+ }
+ }
+ }
+ Charset cs = StandardCharsets.ISO_8859_1;
+ if ( contentEncoding != null ) {
+ try {
+ cs = Charset.forName( contentEncoding );
+ } catch ( Exception e ) {
+ }
+ }
+ //LOGGER.debug("Content type is '" + contentType + "', encoding '" + cs.name() + "'");
+
+ if ( "multipart/form-data".equalsIgnoreCase( contentType ) ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST,
+ "BAD REQUEST: Content type is multipart/form-data, which is not supported" );
+ } else {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte pbuf[] = new byte[ 1000 ];
+ while ( size > 0 ) {
+ int ret = inputStream.read( pbuf, 0, (int)Math.min( size, pbuf.length ) );
+ if ( ret <= 0 )
+ break;
+ if ( ret >= 2 && pbuf[ret - 1] == '\n' && pbuf[ret - 2] == '\r' )
+ break;
+ size -= ret;
+ baos.write( pbuf, 0, ret );
+ }
+ String postLine = new String( baos.toByteArray(), cs );
+ baos.close();
+ // Handle application/x-www-form-urlencoded
+ if ( "application/x-www-form-urlencoded".equalsIgnoreCase( contentType ) ) {
+ decodeParms( postLine, parms );
+ } else if ( files != null && postLine.length() != 0 ) {
+ // Special case for raw POST data => create a special files entry "postData" with raw content data
+ files.put( "postData", postLine );
+ }
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ StringTokenizer st = new StringTokenizer( inLine );
+ if ( !st.hasMoreTokens() ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST,
+ "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
+ }
+
+ pre.put( "method", st.nextToken() );
+
+ if ( !st.hasMoreTokens() ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST,
+ "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
+ }
+
+ String uri = st.nextToken();
+
+ // 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 );
+ }
+
+ // If there's another token, its protocol version,
+ // followed by HTTP headers. Ignore version but parse headers.
+ // NOTE: this now forces header names lower case 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();
+ }
+ }
+
+ pre.put( "uri", uri );
+ } catch ( IOException ioe ) {
+ throw new ResponseException( Response.Status.INTERNAL_ERROR,
+ "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe );
+ }
+ }
+
+ /**
+ * 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 + 3 < rlen ) {
+ if ( buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r'
+ && buf[splitbyte + 3] == '\n' ) {
+ return splitbyte + 4;
+ }
+ splitbyte++;
+ }
+ return 0;
+ }
+
+ /**
+ * 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 ) {
+ queryParameterString = "";
+ return;
+ }
+
+ 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 final Map<String, String> getParms()
+ {
+ return parms;
+ }
+
+ public String getQueryParameterString()
+ {
+ return queryParameterString;
+ }
+
+ @Override
+ public final Map<String, String> getHeaders()
+ {
+ return headers;
+ }
+
+ @Override
+ public final String getUri()
+ {
+ return uri;
+ }
+
+ @Override
+ public final Method getMethod()
+ {
+ return method;
+ }
+
+ @Override
+ public final InputStream getInputStream()
+ {
+ return inputStream;
+ }
+ }
+
+}