summaryrefslogtreecommitdiffstats
path: root/src/main/java/fi/iki/elonen
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/fi/iki/elonen')
-rw-r--r--src/main/java/fi/iki/elonen/ChunkedInputStream.java340
-rw-r--r--src/main/java/fi/iki/elonen/NanoHTTPD.java1214
2 files changed, 1554 insertions, 0 deletions
diff --git a/src/main/java/fi/iki/elonen/ChunkedInputStream.java b/src/main/java/fi/iki/elonen/ChunkedInputStream.java
new file mode 100644
index 0000000..1c0ba8d
--- /dev/null
+++ b/src/main/java/fi/iki/elonen/ChunkedInputStream.java
@@ -0,0 +1,340 @@
+package fi.iki.elonen;
+
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Implements chunked transfer coding. The content is received in small chunks.
+ * Entities transferred using this input stream can be of unlimited length.
+ * After the stream is read to the end, it provides access to the trailers,
+ * if any.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when
+ * {@link #close()} gets called. Instead, it will read until the "end" of its
+ * chunking on close, which allows for the seamless execution of subsequent
+ * HTTP 1.1 requests, while not requiring the client to remember to read the
+ * entire contents of the response.
+ *
+ *
+ * @since 4.0
+ *
+ */
+public class ChunkedInputStream extends InputStream
+{
+
+ private enum State
+ {
+ CHUNK_LEN, CHUNK_DATA, CHUNK_CRLF, CHUNK_INVALID
+ }
+
+ private static final int BUFFER_SIZE = 2048;
+
+ /** The session input buffer */
+ private final InputStream inputStream;
+
+ private State state;
+
+ /** The chunk size */
+ private long chunkSize;
+
+ /** The current position within the current chunk */
+ private long pos;
+
+ /** True if we've reached the end of stream */
+ private boolean eof;
+
+ /** True if this stream is closed */
+ private boolean closed;
+
+ /**
+ * Default constructor.
+ *
+ * @param buffer Session input buffer
+ * @param inputStream Input stream
+ * @param http1Config Message http1Config. If {@code null} {@link Http1Config#DEFAULT} will be
+ * used.
+ *
+ * @since 4.4
+ */
+ public ChunkedInputStream( final InputStream inputStream )
+ {
+ super();
+ this.inputStream = inputStream;
+ this.pos = 0L;
+ this.state = State.CHUNK_LEN;
+ }
+
+ @Override
+ public int available() throws IOException
+ {
+ final int len = this.inputStream.available();
+ return (int)Math.min( len, this.chunkSize - this.pos );
+ }
+
+ /**
+ * <p>
+ * Returns all the data in a chunked stream in coalesced form. A chunk
+ * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0
+ * is detected.
+ * </p>
+ *
+ * <p>
+ * Trailer headers are read automatically at the end of the stream and
+ * can be obtained with the getResponseFooters() method.
+ * </p>
+ *
+ * @return -1 of the end of the stream has been reached or the next data
+ * byte
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read() throws IOException
+ {
+ if ( this.closed ) {
+ throw new StreamClosedException( "Already closed" );
+ }
+ if ( this.eof ) {
+ return -1;
+ }
+ if ( state != State.CHUNK_DATA ) {
+ nextChunk();
+ if ( this.eof ) {
+ return -1;
+ }
+ }
+ final int b = inputStream.read();
+ if ( b != -1 ) {
+ pos++;
+ if ( pos >= chunkSize ) {
+ state = State.CHUNK_CRLF;
+ }
+ return b;
+ }
+ throw new MalformedChunkCodingException( "Truncated chunk (expected size: " + chunkSize + "; actual size: " + pos + ")" );
+ }
+
+ /**
+ * Read some bytes from the stream.
+ *
+ * @param b The byte array that will hold the contents from the stream.
+ * @param off The offset into the byte array at which bytes will start to be
+ * placed.
+ * @param len the maximum number of bytes that can be returned.
+ * @return The number of bytes returned or -1 if the end of stream has been
+ * reached.
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read( final byte[] b, final int off, final int len ) throws IOException
+ {
+ if ( closed ) {
+ throw new StreamClosedException( "Already closed" );
+ }
+
+ if ( eof ) {
+ return -1;
+ }
+ if ( state != State.CHUNK_DATA ) {
+ nextChunk();
+ if ( eof ) {
+ return -1;
+ }
+ }
+ int bytesRead = inputStream.read( b, off, (int)Math.min( len, chunkSize - pos ) );
+ if ( bytesRead != -1 ) {
+ pos += bytesRead;
+ if ( pos >= chunkSize ) {
+ state = State.CHUNK_CRLF;
+ }
+ return bytesRead;
+ }
+ eof = true;
+ throw new MalformedChunkCodingException( "Truncated chunk (expected size: " + chunkSize + "; actual size: " + pos + ")" );
+ }
+
+ /**
+ * Read some bytes from the stream.
+ *
+ * @param b The byte array that will hold the contents from the stream.
+ * @return The number of bytes returned or -1 if the end of stream has been
+ * reached.
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read( final byte[] b ) throws IOException
+ {
+ return read( b, 0, b.length );
+ }
+
+ /**
+ * Read the next chunk.
+ *
+ * @throws IOException in case of an I/O error
+ */
+ private void nextChunk() throws IOException
+ {
+ if ( state == State.CHUNK_INVALID ) {
+ throw new MalformedChunkCodingException( "Corrupt data stream" );
+ }
+ try {
+ chunkSize = getChunkSize();
+ if ( chunkSize < 0L ) {
+ throw new MalformedChunkCodingException( "Negative chunk size" );
+ }
+ state = State.CHUNK_DATA;
+ pos = 0L;
+ if ( chunkSize == 0L ) {
+ eof = true;
+ try {
+ inputStream.read();
+ inputStream.read();
+ } catch ( IOException e ) {
+ throw new MalformedChunkCodingException( "No CRLF after final zero chunk" );
+ }
+ }
+ } catch ( final MalformedChunkCodingException ex ) {
+ state = State.CHUNK_INVALID;
+ throw ex;
+ }
+ }
+
+ /**
+ * Expects the stream to start with a chunksize in hex with optional
+ * comments after a semicolon. The line must end with a CRLF: "a3; some
+ * comment\r\n" Positions the stream at the start of the next line.
+ */
+ private long getChunkSize() throws IOException
+ {
+ int ch;
+ int prevCh;
+ boolean hadSemi;
+ final State st = this.state;
+ switch ( st ) {
+ case CHUNK_CRLF:
+ ch = inputStream.read();
+ ch = ( ch << 8 ) | inputStream.read();
+ if ( ch < 0 ) {
+ throw new MalformedChunkCodingException(
+ "CRLF expected at end of chunk" );
+ }
+ if ( ch != 3338 ) {
+ throw new MalformedChunkCodingException(
+ "Unexpected content at the end of chunk" );
+ }
+ state = State.CHUNK_LEN;
+ //$FALL-THROUGH$
+ case CHUNK_LEN:
+ prevCh = -1;
+ hadSemi = false;
+ StringBuilder sb = new StringBuilder( 8 );
+ for ( ;; ) {
+ ch = inputStream.read();
+ if ( ch == -1 )
+ break;
+ if ( prevCh == 13 && ch == 10 ) {
+ break;
+ }
+ prevCh = ch;
+ if ( ch == 13 )
+ continue;
+ if ( ch == ';' ) {
+ hadSemi = true;
+ }
+ if ( hadSemi )
+ continue;
+ sb.append( (char)ch );
+ }
+ if ( prevCh != 13 || ch != 10 ) {
+ throw new StreamClosedException( "Premature end of chunk coded message body" );
+ }
+ final String s = sb.toString();
+ try {
+ return Long.parseLong( s, 16 );
+ } catch ( final NumberFormatException e ) {
+ throw new MalformedChunkCodingException( "Invalid hex-length in chunk header: " + s );
+ }
+ default:
+ throw new IllegalStateException( "Inconsistent codec state" );
+ }
+ }
+
+ /**
+ * Reads the remainder of the chunked message, leaving the underlying
+ * stream at a position to start reading the next response without
+ * scanning. But does NOT close the underlying stream.
+ *
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public void close() throws IOException
+ {
+ if ( !closed ) {
+ try {
+ if ( !eof && state != State.CHUNK_INVALID ) {
+ // Optimistically check if the content has been fully read
+ // when there's no data remaining in the current chunk.
+ // This is common when self-terminating content (e.g. JSON)
+ // is parsed from response streams.
+ if ( chunkSize == pos && chunkSize > 0 && read() == -1 ) {
+ return;
+ }
+ // read and discard the remainder of the message
+ final byte[] buff = new byte[ BUFFER_SIZE ];
+ while ( read( buff ) >= 0 ) {
+ }
+ }
+ } finally {
+ eof = true;
+ closed = true;
+ }
+ }
+ }
+
+ public static class MalformedChunkCodingException extends IOException
+ {
+ private static final long serialVersionUID = 7092137465179737109L;
+
+ public MalformedChunkCodingException( String msg )
+ {
+ super( msg );
+ }
+ }
+
+ public static class StreamClosedException extends IOException
+ {
+ private static final long serialVersionUID = -5871567871867283867L;
+
+ public StreamClosedException( String msg )
+ {
+ super( msg );
+ }
+ }
+
+}
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..395eca7
--- /dev/null
+++ b/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -0,0 +1,1214 @@
+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.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.TimeUnit;
+
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.openslx.util.GrowingThreadPoolExecutor;
+import org.openslx.util.PrioThreadFactory;
+import org.openslx.util.Util;
+
+/**
+ * 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 ServerSocket myServerSocket;
+ private Set<Socket> openConnections = new HashSet<Socket>();
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ private final ExecutorService asyncRunner;
+
+ protected int maxRequestSize = 0;
+
+ /**
+ * Constructs an HTTP server on given port.
+ */
+ public NanoHTTPD( int port ) throws IOException
+ {
+ this( null, port );
+ }
+
+ /**
+ * @param hostname Address to listen on
+ * @param port Port to listen on
+ */
+ public NanoHTTPD( String hostname, int port ) throws IOException
+ {
+ this( hostname, port, 24, 16 );
+ }
+
+ /**
+ * @param hostname Address to listen on
+ * @param port Port to listen on
+ * @param maxThreads Maximum number of threads to spawn before we start queuing requests
+ * @param maxQueue Maximum number of requests we queue before we start rejecting them with 503
+ */
+ public NanoHTTPD( String hostname, int port, int maxThreads, int maxQueue ) throws IOException
+ {
+ this( hostname, port, new GrowingThreadPoolExecutor( 2, maxThreads, 1, TimeUnit.MINUTES,
+ new ArrayBlockingQueue<Runnable>( maxQueue ), new PrioThreadFactory( "httpd" ) ) );
+ }
+
+ /**
+ * Constructs an HTTP server on given hostname and port.
+ */
+ public NanoHTTPD( String hostname, int port, ExecutorService executor ) throws IOException
+ {
+ this.asyncRunner = executor;
+ myServerSocket = new ServerSocket();
+ myServerSocket.setReuseAddress( true );
+ myServerSocket.bind( ( hostname != null ) ? new InetSocketAddress( hostname, port )
+ : new InetSocketAddress( port ) );
+ }
+
+ /**
+ * Start the server.
+ */
+ @Override
+ public void run()
+ {
+
+ do {
+ try {
+ final Socket sck = myServerSocket.accept();
+ registerConnection( sck );
+ sck.setSoTimeout( SOCKET_READ_TIMEOUT );
+ final InputStream inputStream = sck.getInputStream();
+ try {
+ asyncRunner.execute( new Runnable() {
+ @Override
+ public void run()
+ {
+ OutputStream outputStream = null;
+ try {
+ outputStream = sck.getOutputStream();
+ HTTPSession session = new HTTPSession( inputStream, outputStream,
+ sck.getInetAddress() );
+ while ( !sck.isClosed() && !sck.isInputShutdown() && !sck.isOutputShutdown() ) {
+ 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 {
+ Util.safeClose( outputStream, inputStream, sck );
+ unRegisterConnection( sck );
+ }
+ }
+ } );
+ } catch ( RejectedExecutionException e ) {
+ Util.safeClose( sck, inputStream );
+ unRegisterConnection( sck );
+ }
+ } catch ( IOException e ) {
+ }
+ } while ( !myServerSocket.isClosed() );
+ serverStopped();
+ }
+
+ public void serverStopped()
+ {
+
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop()
+ {
+ try {
+ Util.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 ) {
+ Util.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 )
+ {
+ Method method = session.getMethod();
+ if ( Method.PUT.equals( method ) || Method.POST.equals( method ) ) {
+ try {
+ session.parseBody();
+ } 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, null );
+ }
+
+ /**
+ * 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.
+ //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * 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;
+ }
+ }
+
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * 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, boolean close ) 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, close );
+
+ 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 ) );
+ }
+ Util.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, boolean close )
+ {
+ if ( close ) {
+ if ( !headerAlreadySent( header, "connection" ) ) {
+ sb.append( "Connection: close\r\n" );
+ }
+ return;
+ }
+ // Client wants keep-alive
+ 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" ),
+ SERVICE_UNAVAILABLE( 503, "Service Unavailable" );
+
+ 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() throws IOException, ResponseException;
+ }
+
+ protected class HTTPSession implements IHTTPSession
+ {
+ public static final int BUFSIZE = 8192;
+ private final OutputStream outputStream;
+ private PushbackInputStream inputStream;
+ private InputStream wrappedInput;
+ private int splitbyte;
+ private int rlen;
+ private Method method;
+ private Map<String, String> parms;
+ private Map<String, String> headers;
+ private String queryParameterString;
+ private String remoteIp;
+ private boolean doClose;
+
+ 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>();
+ parms = new HashMap<String, String>();
+ }
+
+ @Override
+ public void execute() throws IOException
+ {
+ doClose = true;
+ 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 || rlen >= BUFSIZE )
+ break;
+ // Try to keep reading as long as we've got less than 8kb
+ 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.clear();
+ 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, splitbyte ) ) );
+
+ // Decode the header into parms and header java properties
+ decodeHeader( hin, parms, headers );
+
+ if ( !Util.isEmptyString( headers.get( "trailer" ) ) ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Trailers not supported." );
+ }
+
+ method = Method.lookup( headers.get( "http-method" ) );
+ if ( method == null ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error." );
+ }
+
+ doClose = "close".equalsIgnoreCase( headers.get( "connection" ) ) || "1.0".equals( headers.get( "http-version" ) );
+ if ( !doClose && method != Method.GET
+ && !headers.containsKey( "content-length" ) && !"chunked".equals( headers.get( "transfer-encoding" ) ) ) {
+ doClose = true;
+ }
+
+ // 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, doClose );
+ }
+ if ( doClose ) {
+ Util.safeClose( outputStream, inputStream );
+ } else {
+ InputStream is = getInputStream();
+ if ( is != null ) {
+ while ( is.read( buf ) > 0 ) {
+ }
+ }
+ }
+ } 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, doClose );
+ Util.safeClose( outputStream );
+ } catch ( ResponseException re ) {
+ Response r = new Response( re.getStatus(), MIME_PLAINTEXT, re.getMessage() );
+ r.send( outputStream, doClose );
+ Util.safeClose( outputStream );
+ }
+ }
+
+ @Override
+ public void parseBody() throws IOException, ResponseException
+ {
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if ( method != Method.POST )
+ return;
+
+ long size;
+ if ( headers.containsKey( "content-length" ) ) {
+ size = Util.parseInt( headers.get( "content-length" ), -1 );
+ } else {
+ size = -1;
+ }
+
+ 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].trim();
+ if ( part.length == 2 ) {
+ contentEncoding = part[1];
+ }
+ }
+ }
+ Charset cs = StandardCharsets.UTF_8;
+ if ( contentEncoding != null ) {
+ try {
+ cs = Charset.forName( contentEncoding );
+ } catch ( Exception e ) {
+ }
+ }
+
+ // Use method here so we get the limited/chunked stream if applicable
+ InputStream is = getInputStream();
+ if ( is == null )
+ return;
+
+ 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 if ( "application/x-www-form-urlencoded".equalsIgnoreCase( contentType ) ) {
+ // Handle application/x-www-form-urlencoded
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte pbuf[] = new byte[ 2000 ];
+ while ( size > 0 ) {
+ int ret = is.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();
+
+ decodeParms( postLine, parms );
+ } // Otherwise leave stream untouched so app can deal with it
+ }
+
+ /**
+ * Decodes the sent headers and loads the data into Key/value pairs
+ */
+ private void decodeHeader( BufferedReader in, 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" );
+ }
+
+ String strMethod = 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.
+ // NOTE: this now forces header names lower case since they are
+ // case insensitive and vary by client.
+ if ( st.hasMoreTokens() ) {
+ String strVersion = st.nextToken();
+ String line = in.readLine();
+ while ( line != null && line.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();
+ }
+ // Add version after, to override headers
+ int sl = strVersion.indexOf( '/' );
+ if ( sl != -1 ) {
+ headers.put( "http-version", strVersion.substring( sl + 1 ) );
+ }
+ }
+
+ headers.put( "http-uri", uri );
+ headers.put( "http-method", strMethod );
+ } catch ( IOException ioe ) {
+ throw new ResponseException( Response.Status.INTERNAL_ERROR,
+ "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe );
+ }
+ }
+
+ /**
+ * Find byte index separating header from body. Return value will point
+ * to first byte after the final \r\n\r\n.
+ */
+ 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 -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.
+ */
+ 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 headers.get( "http-uri" );
+ }
+
+ @Override
+ public final Method getMethod()
+ {
+ return method;
+ }
+
+ @SuppressWarnings( "deprecation" )
+ @Override
+ public final InputStream getInputStream()
+ {
+ if ( method == Method.GET )
+ return null;
+ if ( wrappedInput == null ) {
+ String s = headers.get( "content-length" );
+ if ( s != null ) {
+ long cl = Util.parseLong( s, -1 );
+ if ( cl == 0 )
+ return null;
+ if ( cl < 0 ) {
+ doClose = true;
+ return null;
+ }
+ BoundedInputStream bis = new BoundedInputStream( inputStream, cl );
+ bis.setPropagateClose( false );
+ return wrappedInput = bis;
+ }
+ s = headers.get( "transfer-encoding" );
+ if ( s != null ) {
+ if ( "chunked".equalsIgnoreCase( s.trim() ) )
+ return wrappedInput = new ChunkedInputStream( inputStream );
+ }
+ } else {
+ return wrappedInput;
+ }
+ if ( !doClose )
+ return null;
+ return inputStream;
+ }
+ }
+
+}