1 package be
.nikiroo
.utils
;
7 * Copyright (C) 2012 - 2015 nanohttpd
9 * Redistribution and use in source and binary forms, with or without modification,
10 * are permitted provided that the following conditions are met:
12 * 1. Redistributions of source code must retain the above copyright notice, this
13 * list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright notice,
16 * this list of conditions and the following disclaimer in the documentation
17 * and/or other materials provided with the distribution.
19 * 3. Neither the name of the nanohttpd nor the names of its contributors
20 * may be used to endorse or promote products derived from this software without
21 * specific prior written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32 * OF THE POSSIBILITY OF SUCH DAMAGE.
36 import java
.io
.BufferedInputStream
;
37 import java
.io
.BufferedReader
;
38 import java
.io
.BufferedWriter
;
39 import java
.io
.ByteArrayInputStream
;
40 import java
.io
.ByteArrayOutputStream
;
41 import java
.io
.Closeable
;
42 import java
.io
.DataOutput
;
43 import java
.io
.DataOutputStream
;
45 import java
.io
.FileOutputStream
;
46 import java
.io
.FilterOutputStream
;
47 import java
.io
.IOException
;
48 import java
.io
.InputStream
;
49 import java
.io
.InputStreamReader
;
50 import java
.io
.OutputStream
;
51 import java
.io
.OutputStreamWriter
;
52 import java
.io
.PrintWriter
;
53 import java
.io
.RandomAccessFile
;
54 import java
.io
.UnsupportedEncodingException
;
55 import java
.net
.InetAddress
;
56 import java
.net
.InetSocketAddress
;
57 import java
.net
.ServerSocket
;
58 import java
.net
.Socket
;
59 import java
.net
.SocketException
;
60 import java
.net
.SocketTimeoutException
;
62 import java
.net
.URLDecoder
;
63 import java
.nio
.ByteBuffer
;
64 import java
.nio
.channels
.FileChannel
;
65 import java
.nio
.charset
.Charset
;
66 import java
.nio
.charset
.CharsetEncoder
;
67 import java
.security
.KeyStore
;
68 import java
.text
.SimpleDateFormat
;
69 import java
.util
.ArrayList
;
70 import java
.util
.Calendar
;
71 import java
.util
.Collections
;
72 import java
.util
.Date
;
73 import java
.util
.Enumeration
;
74 import java
.util
.HashMap
;
75 import java
.util
.Iterator
;
76 import java
.util
.List
;
77 import java
.util
.Locale
;
79 import java
.util
.Map
.Entry
;
80 import java
.util
.Properties
;
81 import java
.util
.StringTokenizer
;
82 import java
.util
.TimeZone
;
83 import java
.util
.logging
.Level
;
84 import java
.util
.logging
.Logger
;
85 import java
.util
.regex
.Matcher
;
86 import java
.util
.regex
.Pattern
;
87 import java
.util
.zip
.GZIPOutputStream
;
89 import javax
.net
.ssl
.KeyManager
;
90 import javax
.net
.ssl
.KeyManagerFactory
;
91 import javax
.net
.ssl
.SSLContext
;
92 import javax
.net
.ssl
.SSLException
;
93 import javax
.net
.ssl
.SSLServerSocket
;
94 import javax
.net
.ssl
.SSLServerSocketFactory
;
95 import javax
.net
.ssl
.TrustManagerFactory
;
97 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.IStatus
;
98 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
101 * A simple, tiny, nicely embeddable HTTP server in Java
106 * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen,
107 * 2010 by Konstantinos Togias
111 * <b>Features + limitations: </b>
114 * <li>Only one Java file</li>
115 * <li>Java 5 compatible</li>
116 * <li>Released as open source, Modified BSD licence</li>
117 * <li>No fixed config files, logging, authorization etc. (Implement yourself if
118 * you need them.)</li>
119 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT
120 * support in 1.25)</li>
121 * <li>Supports both dynamic content and file serving</li>
122 * <li>Supports file upload (since version 1.2, 2010)</li>
123 * <li>Supports partial content (streaming)</li>
124 * <li>Supports ETags</li>
125 * <li>Never caches anything</li>
126 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
127 * <li>Default code serves files and shows all HTTP parameters and headers</li>
128 * <li>File server supports directory listing, index.html and index.htm</li>
129 * <li>File server supports partial content (streaming)</li>
130 * <li>File server supports ETags</li>
131 * <li>File server does the 301 redirection trick for directories without '/'</li>
132 * <li>File server supports simple skipping for files (continue download)</li>
133 * <li>File server serves also very long files without memory overhead</li>
134 * <li>Contains a built-in list of most common MIME types</li>
135 * <li>All header names are converted to lower case so they don't vary between
136 * browsers/clients</li>
141 * <b>How to use: </b>
144 * <li>Subclass and implement serve() and embed to your own program</li>
148 * See the separate "LICENSE.md" file for the distribution license (Modified BSD
151 public abstract class NanoHTTPD
{
154 * Pluggable strategy for asynchronously executing requests.
156 public interface AsyncRunner
{
160 void closed(ClientHandler clientHandler
);
162 void exec(ClientHandler code
);
166 * The runnable that will be used for every new client connection.
168 public class ClientHandler
implements Runnable
{
170 private final InputStream inputStream
;
172 private final Socket acceptSocket
;
174 public ClientHandler(InputStream inputStream
, Socket acceptSocket
) {
175 this.inputStream
= inputStream
;
176 this.acceptSocket
= acceptSocket
;
179 public void close() {
180 safeClose(this.inputStream
);
181 safeClose(this.acceptSocket
);
186 OutputStream outputStream
= null;
188 outputStream
= this.acceptSocket
.getOutputStream();
189 TempFileManager tempFileManager
= NanoHTTPD
.this.tempFileManagerFactory
.create();
190 HTTPSession session
= new HTTPSession(tempFileManager
, this.inputStream
, outputStream
, this.acceptSocket
.getInetAddress());
191 while (!this.acceptSocket
.isClosed()) {
194 } catch (Exception e
) {
195 // When the socket is closed by the client,
196 // we throw our own SocketException
197 // to break the "keep alive" loop above. If
198 // the exception was anything other
199 // than the expected SocketException OR a
200 // SocketTimeoutException, print the
202 if (!(e
instanceof SocketException
&& "NanoHttpd Shutdown".equals(e
.getMessage())) && !(e
instanceof SocketTimeoutException
)) {
203 NanoHTTPD
.LOG
.log(Level
.SEVERE
, "Communication with the client broken, or an bug in the handler code", e
);
206 safeClose(outputStream
);
207 safeClose(this.inputStream
);
208 safeClose(this.acceptSocket
);
209 NanoHTTPD
.this.asyncRunner
.closed(this);
214 public static class Cookie
{
216 public static String
getHTTPTime(int days
) {
217 Calendar calendar
= Calendar
.getInstance();
218 SimpleDateFormat dateFormat
= new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale
.US
);
219 dateFormat
.setTimeZone(TimeZone
.getTimeZone("GMT"));
220 calendar
.add(Calendar
.DAY_OF_MONTH
, days
);
221 return dateFormat
.format(calendar
.getTime());
224 private final String n
, v
, e
;
226 public Cookie(String name
, String value
) {
227 this(name
, value
, 30);
230 public Cookie(String name
, String value
, int numDays
) {
233 this.e
= getHTTPTime(numDays
);
236 public Cookie(String name
, String value
, String expires
) {
242 public String
getHTTPHeader() {
243 String fmt
= "%s=%s; expires=%s";
244 return String
.format(fmt
, this.n
, this.v
, this.e
);
249 * Provides rudimentary support for cookies. Doesn't support 'path',
250 * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported
255 public class CookieHandler
implements Iterable
<String
> {
257 private final HashMap
<String
, String
> cookies
= new HashMap
<String
, String
>();
259 private final ArrayList
<Cookie
> queue
= new ArrayList
<Cookie
>();
261 public CookieHandler(Map
<String
, String
> httpHeaders
) {
262 String raw
= httpHeaders
.get("cookie");
264 String
[] tokens
= raw
.split(";");
265 for (String token
: tokens
) {
266 String
[] data
= token
.trim().split("=");
267 if (data
.length
== 2) {
268 this.cookies
.put(data
[0], data
[1]);
275 * Set a cookie with an expiration date from a month ago, effectively
276 * deleting it on the client side.
281 public void delete(String name
) {
282 set(name
, "-delete-", -30);
286 public Iterator
<String
> iterator() {
287 return this.cookies
.keySet().iterator();
291 * Read a cookie from the HTTP Headers.
295 * @return The cookie's value if it exists, null otherwise.
297 public String
read(String name
) {
298 return this.cookies
.get(name
);
301 public void set(Cookie cookie
) {
302 this.queue
.add(cookie
);
311 * The cookie's value.
313 * How many days until the cookie expires.
315 public void set(String name
, String value
, int expires
) {
316 this.queue
.add(new Cookie(name
, value
, Cookie
.getHTTPTime(expires
)));
320 * Internally used by the webserver to add all queued cookies into the
321 * Response's HTTP Headers.
324 * The Response object to which headers the queued cookies
327 public void unloadQueue(Response response
) {
328 for (Cookie cookie
: this.queue
) {
329 response
.addHeader("Set-Cookie", cookie
.getHTTPHeader());
335 * Default threading strategy for NanoHTTPD.
338 * By default, the server spawns a new Thread for every incoming request.
339 * These are set to <i>daemon</i> status, and named according to the request
340 * number. The name is useful when profiling the application.
343 public static class DefaultAsyncRunner
implements AsyncRunner
{
345 private long requestCount
;
347 private final List
<ClientHandler
> running
= Collections
.synchronizedList(new ArrayList
<NanoHTTPD
.ClientHandler
>());
350 * @return a list with currently running clients.
352 public List
<ClientHandler
> getRunning() {
357 public void closeAll() {
358 // copy of the list for concurrency
359 for (ClientHandler clientHandler
: new ArrayList
<ClientHandler
>(this.running
)) {
360 clientHandler
.close();
365 public void closed(ClientHandler clientHandler
) {
366 this.running
.remove(clientHandler
);
370 public void exec(ClientHandler clientHandler
) {
372 Thread t
= new Thread(clientHandler
);
374 t
.setName("NanoHttpd Request Processor (#" + this.requestCount
+ ")");
375 this.running
.add(clientHandler
);
381 * Default strategy for creating and cleaning up temporary files.
384 * By default, files are created by <code>File.createTempFile()</code> in
385 * the directory specified.
388 public static class DefaultTempFile
implements TempFile
{
390 private final File file
;
392 private final OutputStream fstream
;
394 public DefaultTempFile(File tempdir
) throws IOException
{
395 this.file
= File
.createTempFile("NanoHTTPD-", "", tempdir
);
396 this.fstream
= new FileOutputStream(this.file
);
400 public void delete() throws Exception
{
401 safeClose(this.fstream
);
402 if (!this.file
.delete()) {
403 throw new Exception("could not delete temporary file: " + this.file
.getAbsolutePath());
408 public String
getName() {
409 return this.file
.getAbsolutePath();
413 public OutputStream
open() throws Exception
{
419 * Default strategy for creating and cleaning up temporary files.
422 * This class stores its files in the standard location (that is, wherever
423 * <code>java.io.tmpdir</code> points to). Files are added to an internal
424 * list, and deleted when no longer needed (that is, when
425 * <code>clear()</code> is invoked at the end of processing a request).
428 public static class DefaultTempFileManager
implements TempFileManager
{
430 private final File tmpdir
;
432 private final List
<TempFile
> tempFiles
;
434 public DefaultTempFileManager() {
435 this.tmpdir
= new File(System
.getProperty("java.io.tmpdir"));
436 if (!tmpdir
.exists()) {
439 this.tempFiles
= new ArrayList
<TempFile
>();
443 public void clear() {
444 for (TempFile file
: this.tempFiles
) {
447 } catch (Exception ignored
) {
448 NanoHTTPD
.LOG
.log(Level
.WARNING
, "could not delete file ", ignored
);
451 this.tempFiles
.clear();
455 public TempFile
createTempFile(String filename_hint
) throws Exception
{
456 DefaultTempFile tempFile
= new DefaultTempFile(this.tmpdir
);
457 this.tempFiles
.add(tempFile
);
463 * Default strategy for creating and cleaning up temporary files.
465 private class DefaultTempFileManagerFactory
implements TempFileManagerFactory
{
468 public TempFileManager
create() {
469 return new DefaultTempFileManager();
474 * Creates a normal ServerSocket for TCP connections
476 public static class DefaultServerSocketFactory
implements ServerSocketFactory
{
479 public ServerSocket
create() throws IOException
{
480 return new ServerSocket();
486 * Creates a new SSLServerSocket
488 public static class SecureServerSocketFactory
implements ServerSocketFactory
{
490 private SSLServerSocketFactory sslServerSocketFactory
;
492 private String
[] sslProtocols
;
494 public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory
, String
[] sslProtocols
) {
495 this.sslServerSocketFactory
= sslServerSocketFactory
;
496 this.sslProtocols
= sslProtocols
;
500 public ServerSocket
create() throws IOException
{
501 SSLServerSocket ss
= null;
502 ss
= (SSLServerSocket
) this.sslServerSocketFactory
.createServerSocket();
503 if (this.sslProtocols
!= null) {
504 ss
.setEnabledProtocols(this.sslProtocols
);
506 ss
.setEnabledProtocols(ss
.getSupportedProtocols());
508 ss
.setUseClientMode(false);
509 ss
.setWantClientAuth(false);
510 ss
.setNeedClientAuth(false);
516 private static final String CONTENT_DISPOSITION_REGEX
= "([ |\t]*Content-Disposition[ |\t]*:)(.*)";
518 private static final Pattern CONTENT_DISPOSITION_PATTERN
= Pattern
.compile(CONTENT_DISPOSITION_REGEX
, Pattern
.CASE_INSENSITIVE
);
520 private static final String CONTENT_TYPE_REGEX
= "([ |\t]*content-type[ |\t]*:)(.*)";
522 private static final Pattern CONTENT_TYPE_PATTERN
= Pattern
.compile(CONTENT_TYPE_REGEX
, Pattern
.CASE_INSENSITIVE
);
524 private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX
= "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]";
526 private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN
= Pattern
.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX
);
528 protected static class ContentType
{
530 private static final String ASCII_ENCODING
= "US-ASCII";
532 private static final String MULTIPART_FORM_DATA_HEADER
= "multipart/form-data";
534 private static final String CONTENT_REGEX
= "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)";
536 private static final Pattern MIME_PATTERN
= Pattern
.compile(CONTENT_REGEX
, Pattern
.CASE_INSENSITIVE
);
538 private static final String CHARSET_REGEX
= "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?";
540 private static final Pattern CHARSET_PATTERN
= Pattern
.compile(CHARSET_REGEX
, Pattern
.CASE_INSENSITIVE
);
542 private static final String BOUNDARY_REGEX
= "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?";
544 private static final Pattern BOUNDARY_PATTERN
= Pattern
.compile(BOUNDARY_REGEX
, Pattern
.CASE_INSENSITIVE
);
546 private final String contentTypeHeader
;
548 private final String contentType
;
550 private final String encoding
;
552 private final String boundary
;
554 public ContentType(String contentTypeHeader
) {
555 this.contentTypeHeader
= contentTypeHeader
;
556 if (contentTypeHeader
!= null) {
557 contentType
= getDetailFromContentHeader(contentTypeHeader
, MIME_PATTERN
, "", 1);
558 encoding
= getDetailFromContentHeader(contentTypeHeader
, CHARSET_PATTERN
, null, 2);
563 if (MULTIPART_FORM_DATA_HEADER
.equalsIgnoreCase(contentType
)) {
564 boundary
= getDetailFromContentHeader(contentTypeHeader
, BOUNDARY_PATTERN
, null, 2);
570 private String
getDetailFromContentHeader(String contentTypeHeader
, Pattern pattern
, String defaultValue
, int group
) {
571 Matcher matcher
= pattern
.matcher(contentTypeHeader
);
572 return matcher
.find() ? matcher
.group(group
) : defaultValue
;
575 public String
getContentTypeHeader() {
576 return contentTypeHeader
;
579 public String
getContentType() {
583 public String
getEncoding() {
584 return encoding
== null ? ASCII_ENCODING
: encoding
;
587 public String
getBoundary() {
591 public boolean isMultipart() {
592 return MULTIPART_FORM_DATA_HEADER
.equalsIgnoreCase(contentType
);
595 public ContentType
tryUTF8() {
596 if (encoding
== null) {
597 return new ContentType(this.contentTypeHeader
+ "; charset=UTF-8");
603 protected class HTTPSession
implements IHTTPSession
{
605 private static final int REQUEST_BUFFER_LEN
= 512;
607 private static final int MEMORY_STORE_LIMIT
= 1024;
609 public static final int BUFSIZE
= 8192;
611 public static final int MAX_HEADER_SIZE
= 1024;
613 private final TempFileManager tempFileManager
;
615 private final OutputStream outputStream
;
617 private final BufferedInputStream inputStream
;
619 private int splitbyte
;
625 private Method method
;
627 private Map
<String
, List
<String
>> parms
;
629 private Map
<String
, String
> headers
;
631 private CookieHandler cookies
;
633 private String queryParameterString
;
635 private String remoteIp
;
637 private String remoteHostname
;
639 private String protocolVersion
;
641 public HTTPSession(TempFileManager tempFileManager
, InputStream inputStream
, OutputStream outputStream
) {
642 this.tempFileManager
= tempFileManager
;
643 this.inputStream
= new BufferedInputStream(inputStream
, HTTPSession
.BUFSIZE
);
644 this.outputStream
= outputStream
;
647 public HTTPSession(TempFileManager tempFileManager
, InputStream inputStream
, OutputStream outputStream
, InetAddress inetAddress
) {
648 this.tempFileManager
= tempFileManager
;
649 this.inputStream
= new BufferedInputStream(inputStream
, HTTPSession
.BUFSIZE
);
650 this.outputStream
= outputStream
;
651 this.remoteIp
= inetAddress
.isLoopbackAddress() || inetAddress
.isAnyLocalAddress() ?
"127.0.0.1" : inetAddress
.getHostAddress().toString();
652 this.remoteHostname
= inetAddress
.isLoopbackAddress() || inetAddress
.isAnyLocalAddress() ?
"localhost" : inetAddress
.getHostName().toString();
653 this.headers
= new HashMap
<String
, String
>();
657 * Decodes the sent headers and loads the data into Key/value pairs
659 private void decodeHeader(BufferedReader in
, Map
<String
, String
> pre
, Map
<String
, List
<String
>> parms
, Map
<String
, String
> headers
) throws ResponseException
{
661 // Read the request line
662 String inLine
= in
.readLine();
663 if (inLine
== null) {
667 StringTokenizer st
= new StringTokenizer(inLine
);
668 if (!st
.hasMoreTokens()) {
669 throw new ResponseException(Response
.Status
.BAD_REQUEST
, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
672 pre
.put("method", st
.nextToken());
674 if (!st
.hasMoreTokens()) {
675 throw new ResponseException(Response
.Status
.BAD_REQUEST
, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
678 String uri
= st
.nextToken();
680 // Decode parameters from the URI
681 int qmi
= uri
.indexOf('?');
683 decodeParms(uri
.substring(qmi
+ 1), parms
);
684 uri
= decodePercent(uri
.substring(0, qmi
));
686 uri
= decodePercent(uri
);
689 // If there's another token, its protocol version,
690 // followed by HTTP headers.
691 // NOTE: this now forces header names lower case since they are
692 // case insensitive and vary by client.
693 if (st
.hasMoreTokens()) {
694 protocolVersion
= st
.nextToken();
696 protocolVersion
= "HTTP/1.1";
697 NanoHTTPD
.LOG
.log(Level
.FINE
, "no protocol version specified, strange. Assuming HTTP/1.1.");
699 String line
= in
.readLine();
700 while (line
!= null && !line
.trim().isEmpty()) {
701 int p
= line
.indexOf(':');
703 headers
.put(line
.substring(0, p
).trim().toLowerCase(Locale
.US
), line
.substring(p
+ 1).trim());
705 line
= in
.readLine();
709 } catch (IOException ioe
) {
710 throw new ResponseException(Response
.Status
.INTERNAL_ERROR
, "SERVER INTERNAL ERROR: IOException: " + ioe
.getMessage(), ioe
);
715 * Decodes the Multipart Body data and put it into Key/Value pairs.
717 private void decodeMultipartFormData(ContentType contentType
, ByteBuffer fbuf
, Map
<String
, List
<String
>> parms
, Map
<String
, String
> files
) throws ResponseException
{
720 int[] boundaryIdxs
= getBoundaryPositions(fbuf
, contentType
.getBoundary().getBytes());
721 if (boundaryIdxs
.length
< 2) {
722 throw new ResponseException(Response
.Status
.BAD_REQUEST
, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
725 byte[] partHeaderBuff
= new byte[MAX_HEADER_SIZE
];
726 for (int boundaryIdx
= 0; boundaryIdx
< boundaryIdxs
.length
- 1; boundaryIdx
++) {
727 fbuf
.position(boundaryIdxs
[boundaryIdx
]);
728 int len
= (fbuf
.remaining() < MAX_HEADER_SIZE
) ? fbuf
.remaining() : MAX_HEADER_SIZE
;
729 fbuf
.get(partHeaderBuff
, 0, len
);
731 new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff
, 0, len
), Charset
.forName(contentType
.getEncoding())), len
);
734 // First line is boundary string
735 String mpline
= in
.readLine();
737 if (mpline
== null || !mpline
.contains(contentType
.getBoundary())) {
738 throw new ResponseException(Response
.Status
.BAD_REQUEST
, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
741 String partName
= null, fileName
= null, partContentType
= null;
742 // Parse the reset of the header lines
743 mpline
= in
.readLine();
745 while (mpline
!= null && mpline
.trim().length() > 0) {
746 Matcher matcher
= CONTENT_DISPOSITION_PATTERN
.matcher(mpline
);
747 if (matcher
.matches()) {
748 String attributeString
= matcher
.group(2);
749 matcher
= CONTENT_DISPOSITION_ATTRIBUTE_PATTERN
.matcher(attributeString
);
750 while (matcher
.find()) {
751 String key
= matcher
.group(1);
752 if ("name".equalsIgnoreCase(key
)) {
753 partName
= matcher
.group(2);
754 } else if ("filename".equalsIgnoreCase(key
)) {
755 fileName
= matcher
.group(2);
756 // add these two line to support multiple
757 // files uploaded using the same field Id
758 if (!fileName
.isEmpty()) {
760 partName
= partName
+ String
.valueOf(pcount
++);
767 matcher
= CONTENT_TYPE_PATTERN
.matcher(mpline
);
768 if (matcher
.matches()) {
769 partContentType
= matcher
.group(2).trim();
771 mpline
= in
.readLine();
774 int partHeaderLength
= 0;
775 while (headerLines
-- > 0) {
776 partHeaderLength
= scipOverNewLine(partHeaderBuff
, partHeaderLength
);
778 // Read the part data
779 if (partHeaderLength
>= len
- 4) {
780 throw new ResponseException(Response
.Status
.INTERNAL_ERROR
, "Multipart header size exceeds MAX_HEADER_SIZE.");
782 int partDataStart
= boundaryIdxs
[boundaryIdx
] + partHeaderLength
;
783 int partDataEnd
= boundaryIdxs
[boundaryIdx
+ 1] - 4;
785 fbuf
.position(partDataStart
);
787 List
<String
> values
= parms
.get(partName
);
788 if (values
== null) {
789 values
= new ArrayList
<String
>();
790 parms
.put(partName
, values
);
793 if (partContentType
== null) {
794 // Read the part into a string
795 byte[] data_bytes
= new byte[partDataEnd
- partDataStart
];
796 fbuf
.get(data_bytes
);
798 values
.add(new String(data_bytes
, contentType
.getEncoding()));
800 // Read it into a file
801 String path
= saveTmpFile(fbuf
, partDataStart
, partDataEnd
- partDataStart
, fileName
);
802 if (!files
.containsKey(partName
)) {
803 files
.put(partName
, path
);
806 while (files
.containsKey(partName
+ count
)) {
809 files
.put(partName
+ count
, path
);
811 values
.add(fileName
);
814 } catch (ResponseException re
) {
816 } catch (Exception e
) {
817 throw new ResponseException(Response
.Status
.INTERNAL_ERROR
, e
.toString());
821 private int scipOverNewLine(byte[] partHeaderBuff
, int index
) {
822 while (partHeaderBuff
[index
] != '\n') {
829 * Decodes parameters in percent-encoded URI-format ( e.g.
830 * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
833 private void decodeParms(String parms
, Map
<String
, List
<String
>> p
) {
835 this.queryParameterString
= "";
839 this.queryParameterString
= parms
;
840 StringTokenizer st
= new StringTokenizer(parms
, "&");
841 while (st
.hasMoreTokens()) {
842 String e
= st
.nextToken();
843 int sep
= e
.indexOf('=');
848 key
= decodePercent(e
.substring(0, sep
)).trim();
849 value
= decodePercent(e
.substring(sep
+ 1));
851 key
= decodePercent(e
).trim();
855 List
<String
> values
= p
.get(key
);
856 if (values
== null) {
857 values
= new ArrayList
<String
>();
866 public void execute() throws IOException
{
869 // Read the first 8192 bytes.
870 // The full header should fit in here.
871 // Apache's default header limit is 8KB.
872 // Do NOT assume that a single read will get the entire header
874 byte[] buf
= new byte[HTTPSession
.BUFSIZE
];
879 this.inputStream
.mark(HTTPSession
.BUFSIZE
);
881 read
= this.inputStream
.read(buf
, 0, HTTPSession
.BUFSIZE
);
882 } catch (SSLException e
) {
884 } catch (IOException e
) {
885 safeClose(this.inputStream
);
886 safeClose(this.outputStream
);
887 throw new SocketException("NanoHttpd Shutdown");
890 // socket was been closed
891 safeClose(this.inputStream
);
892 safeClose(this.outputStream
);
893 throw new SocketException("NanoHttpd Shutdown");
897 this.splitbyte
= findHeaderEnd(buf
, this.rlen
);
898 if (this.splitbyte
> 0) {
901 read
= this.inputStream
.read(buf
, this.rlen
, HTTPSession
.BUFSIZE
- this.rlen
);
904 if (this.splitbyte
< this.rlen
) {
905 this.inputStream
.reset();
906 this.inputStream
.skip(this.splitbyte
);
909 this.parms
= new HashMap
<String
, List
<String
>>();
910 if (null == this.headers
) {
911 this.headers
= new HashMap
<String
, String
>();
913 this.headers
.clear();
916 // Create a BufferedReader for parsing the header.
917 BufferedReader hin
= new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf
, 0, this.rlen
)));
919 // Decode the header into parms and header java properties
920 Map
<String
, String
> pre
= new HashMap
<String
, String
>();
921 decodeHeader(hin
, pre
, this.parms
, this.headers
);
923 if (null != this.remoteIp
) {
924 this.headers
.put("remote-addr", this.remoteIp
);
925 this.headers
.put("http-client-ip", this.remoteIp
);
928 this.method
= Method
.lookup(pre
.get("method"));
929 if (this.method
== null) {
930 throw new ResponseException(Response
.Status
.BAD_REQUEST
, "BAD REQUEST: Syntax error. HTTP verb " + pre
.get("method") + " unhandled.");
933 this.uri
= pre
.get("uri");
935 this.cookies
= new CookieHandler(this.headers
);
937 String connection
= this.headers
.get("connection");
938 boolean keepAlive
= "HTTP/1.1".equals(protocolVersion
) && (connection
== null || !connection
.matches("(?i).*close.*"));
940 // Ok, now do the serve()
942 // TODO: long body_size = getBodySize();
943 // TODO: long pos_before_serve = this.inputStream.totalRead()
944 // (requires implementation for totalRead())
946 // TODO: this.inputStream.skip(body_size -
947 // (this.inputStream.totalRead() - pos_before_serve))
950 throw new ResponseException(Response
.Status
.INTERNAL_ERROR
, "SERVER INTERNAL ERROR: Serve() returned a null response.");
952 String acceptEncoding
= this.headers
.get("accept-encoding");
953 this.cookies
.unloadQueue(r
);
954 r
.setRequestMethod(this.method
);
955 r
.setGzipEncoding(useGzipWhenAccepted(r
) && acceptEncoding
!= null && acceptEncoding
.contains("gzip"));
956 r
.setKeepAlive(keepAlive
);
957 r
.send(this.outputStream
);
959 if (!keepAlive
|| r
.isCloseConnection()) {
960 throw new SocketException("NanoHttpd Shutdown");
962 } catch (SocketException e
) {
963 // throw it out to close socket object (finalAccept)
965 } catch (SocketTimeoutException ste
) {
966 // treat socket timeouts the same way we treat socket exceptions
967 // i.e. close the stream & finalAccept object by throwing the
968 // exception up the call stack.
970 } catch (SSLException ssle
) {
971 Response resp
= newFixedLengthResponse(Response
.Status
.INTERNAL_ERROR
, NanoHTTPD
.MIME_PLAINTEXT
, "SSL PROTOCOL FAILURE: " + ssle
.getMessage());
972 resp
.send(this.outputStream
);
973 safeClose(this.outputStream
);
974 } catch (IOException ioe
) {
975 Response resp
= newFixedLengthResponse(Response
.Status
.INTERNAL_ERROR
, NanoHTTPD
.MIME_PLAINTEXT
, "SERVER INTERNAL ERROR: IOException: " + ioe
.getMessage());
976 resp
.send(this.outputStream
);
977 safeClose(this.outputStream
);
978 } catch (ResponseException re
) {
979 Response resp
= newFixedLengthResponse(re
.getStatus(), NanoHTTPD
.MIME_PLAINTEXT
, re
.getMessage());
980 resp
.send(this.outputStream
);
981 safeClose(this.outputStream
);
984 this.tempFileManager
.clear();
989 * Find byte index separating header from body. It must be the last byte
990 * of the first two sequential new lines.
992 private int findHeaderEnd(final byte[] buf
, int rlen
) {
994 while (splitbyte
+ 1 < rlen
) {
997 if (buf
[splitbyte
] == '\r' && buf
[splitbyte
+ 1] == '\n' && splitbyte
+ 3 < rlen
&& buf
[splitbyte
+ 2] == '\r' && buf
[splitbyte
+ 3] == '\n') {
998 return splitbyte
+ 4;
1002 if (buf
[splitbyte
] == '\n' && buf
[splitbyte
+ 1] == '\n') {
1003 return splitbyte
+ 2;
1011 * Find the byte positions where multipart boundaries start. This reads
1012 * a large block at a time and uses a temporary buffer to optimize
1013 * (memory mapped) file access.
1015 private int[] getBoundaryPositions(ByteBuffer b
, byte[] boundary
) {
1016 int[] res
= new int[0];
1017 if (b
.remaining() < boundary
.length
) {
1021 int search_window_pos
= 0;
1022 byte[] search_window
= new byte[4 * 1024 + boundary
.length
];
1024 int first_fill
= (b
.remaining() < search_window
.length
) ? b
.remaining() : search_window
.length
;
1025 b
.get(search_window
, 0, first_fill
);
1026 int new_bytes
= first_fill
- boundary
.length
;
1029 // Search the search_window
1030 for (int j
= 0; j
< new_bytes
; j
++) {
1031 for (int i
= 0; i
< boundary
.length
; i
++) {
1032 if (search_window
[j
+ i
] != boundary
[i
])
1034 if (i
== boundary
.length
- 1) {
1035 // Match found, add it to results
1036 int[] new_res
= new int[res
.length
+ 1];
1037 System
.arraycopy(res
, 0, new_res
, 0, res
.length
);
1038 new_res
[res
.length
] = search_window_pos
+ j
;
1043 search_window_pos
+= new_bytes
;
1045 // Copy the end of the buffer to the start
1046 System
.arraycopy(search_window
, search_window
.length
- boundary
.length
, search_window
, 0, boundary
.length
);
1048 // Refill search_window
1049 new_bytes
= search_window
.length
- boundary
.length
;
1050 new_bytes
= (b
.remaining() < new_bytes
) ? b
.remaining() : new_bytes
;
1051 b
.get(search_window
, boundary
.length
, new_bytes
);
1052 } while (new_bytes
> 0);
1057 public CookieHandler
getCookies() {
1058 return this.cookies
;
1062 public final Map
<String
, String
> getHeaders() {
1063 return this.headers
;
1067 public final InputStream
getInputStream() {
1068 return this.inputStream
;
1072 public final Method
getMethod() {
1077 * @deprecated use {@link #getParameters()} instead.
1081 public final Map
<String
, String
> getParms() {
1082 Map
<String
, String
> result
= new HashMap
<String
, String
>();
1083 for (String key
: this.parms
.keySet()) {
1084 result
.put(key
, this.parms
.get(key
).get(0));
1091 public final Map
<String
, List
<String
>> getParameters() {
1096 public String
getQueryParameterString() {
1097 return this.queryParameterString
;
1100 private RandomAccessFile
getTmpBucket() {
1102 TempFile tempFile
= this.tempFileManager
.createTempFile(null);
1103 return new RandomAccessFile(tempFile
.getName(), "rw");
1104 } catch (Exception e
) {
1105 throw new Error(e
); // we won't recover, so throw an error
1110 public final String
getUri() {
1115 * Deduce body length in bytes. Either from "content-length" header or
1118 public long getBodySize() {
1119 if (this.headers
.containsKey("content-length")) {
1120 return Long
.parseLong(this.headers
.get("content-length"));
1121 } else if (this.splitbyte
< this.rlen
) {
1122 return this.rlen
- this.splitbyte
;
1128 public void parseBody(Map
<String
, String
> files
) throws IOException
, ResponseException
{
1129 RandomAccessFile randomAccessFile
= null;
1131 long size
= getBodySize();
1132 ByteArrayOutputStream baos
= null;
1133 DataOutput requestDataOutput
= null;
1135 // Store the request in memory or a file, depending on size
1136 if (size
< MEMORY_STORE_LIMIT
) {
1137 baos
= new ByteArrayOutputStream();
1138 requestDataOutput
= new DataOutputStream(baos
);
1140 randomAccessFile
= getTmpBucket();
1141 requestDataOutput
= randomAccessFile
;
1144 // Read all the body and write it to request_data_output
1145 byte[] buf
= new byte[REQUEST_BUFFER_LEN
];
1146 while (this.rlen
>= 0 && size
> 0) {
1147 this.rlen
= this.inputStream
.read(buf
, 0, (int) Math
.min(size
, REQUEST_BUFFER_LEN
));
1149 if (this.rlen
> 0) {
1150 requestDataOutput
.write(buf
, 0, this.rlen
);
1154 ByteBuffer fbuf
= null;
1156 fbuf
= ByteBuffer
.wrap(baos
.toByteArray(), 0, baos
.size());
1158 fbuf
= randomAccessFile
.getChannel().map(FileChannel
.MapMode
.READ_ONLY
, 0, randomAccessFile
.length());
1159 randomAccessFile
.seek(0);
1162 // If the method is POST, there may be parameters
1163 // in data section, too, read it:
1164 if (Method
.POST
.equals(this.method
)) {
1165 ContentType contentType
= new ContentType(this.headers
.get("content-type"));
1166 if (contentType
.isMultipart()) {
1167 String boundary
= contentType
.getBoundary();
1168 if (boundary
== null) {
1169 throw new ResponseException(Response
.Status
.BAD_REQUEST
,
1170 "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
1172 decodeMultipartFormData(contentType
, fbuf
, this.parms
, files
);
1174 byte[] postBytes
= new byte[fbuf
.remaining()];
1175 fbuf
.get(postBytes
);
1176 String postLine
= new String(postBytes
, contentType
.getEncoding()).trim();
1177 // Handle application/x-www-form-urlencoded
1178 if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType
.getContentType())) {
1179 decodeParms(postLine
, this.parms
);
1180 } else if (postLine
.length() != 0) {
1181 // Special case for raw POST data => create a
1182 // special files entry "postData" with raw content
1184 files
.put("postData", postLine
);
1187 } else if (Method
.PUT
.equals(this.method
)) {
1188 files
.put("content", saveTmpFile(fbuf
, 0, fbuf
.limit(), null));
1191 safeClose(randomAccessFile
);
1196 * Retrieves the content of a sent file and saves it to a temporary
1197 * file. The full path to the saved file is returned.
1199 private String
saveTmpFile(ByteBuffer b
, int offset
, int len
, String filename_hint
) {
1202 FileOutputStream fileOutputStream
= null;
1204 TempFile tempFile
= this.tempFileManager
.createTempFile(filename_hint
);
1205 ByteBuffer src
= b
.duplicate();
1206 fileOutputStream
= new FileOutputStream(tempFile
.getName());
1207 FileChannel dest
= fileOutputStream
.getChannel();
1208 src
.position(offset
).limit(offset
+ len
);
1209 dest
.write(src
.slice());
1210 path
= tempFile
.getName();
1211 } catch (Exception e
) { // Catch exception if any
1212 throw new Error(e
); // we won't recover, so throw an error
1214 safeClose(fileOutputStream
);
1221 public String
getRemoteIpAddress() {
1222 return this.remoteIp
;
1226 public String
getRemoteHostName() {
1227 return this.remoteHostname
;
1232 * Handles one session, i.e. parses the HTTP request and returns the
1235 public interface IHTTPSession
{
1237 void execute() throws IOException
;
1239 CookieHandler
getCookies();
1241 Map
<String
, String
> getHeaders();
1243 InputStream
getInputStream();
1248 * This method will only return the first value for a given parameter.
1249 * You will want to use getParameters if you expect multiple values for
1252 * @deprecated use {@link #getParameters()} instead.
1255 Map
<String
, String
> getParms();
1257 Map
<String
, List
<String
>> getParameters();
1259 String
getQueryParameterString();
1262 * @return the path part of the URL.
1267 * Adds the files in the request body to the files map.
1272 void parseBody(Map
<String
, String
> files
) throws IOException
, ResponseException
;
1275 * Get the remote ip address of the requester.
1277 * @return the IP address.
1279 String
getRemoteIpAddress();
1282 * Get the remote hostname of the requester.
1284 * @return the hostname.
1286 String
getRemoteHostName();
1290 * HTTP Request methods, with the ability to decode a <code>String</code>
1291 * back to its enum value.
1293 public enum Method
{
1311 static Method
lookup(String method
) {
1316 return valueOf(method
);
1317 } catch (IllegalArgumentException e
) {
1325 * HTTP response. Return one of these from serve().
1327 public static class Response
implements Closeable
{
1329 public interface IStatus
{
1331 String
getDescription();
1333 int getRequestStatus();
1337 * Some HTTP response status codes
1339 public enum Status
implements IStatus
{
1340 SWITCH_PROTOCOL(101, "Switching Protocols"),
1343 CREATED(201, "Created"),
1344 ACCEPTED(202, "Accepted"),
1345 NO_CONTENT(204, "No Content"),
1346 PARTIAL_CONTENT(206, "Partial Content"),
1347 MULTI_STATUS(207, "Multi-Status"),
1349 REDIRECT(301, "Moved Permanently"),
1351 * Many user agents mishandle 302 in ways that violate the RFC1945
1352 * spec (i.e., redirect a POST to a GET). 303 and 307 were added in
1353 * RFC2616 to address this. You should prefer 303 and 307 unless the
1354 * calling user agent does not support 303 and 307 functionality
1357 FOUND(302, "Found"),
1358 REDIRECT_SEE_OTHER(303, "See Other"),
1359 NOT_MODIFIED(304, "Not Modified"),
1360 TEMPORARY_REDIRECT(307, "Temporary Redirect"),
1362 BAD_REQUEST(400, "Bad Request"),
1363 UNAUTHORIZED(401, "Unauthorized"),
1364 FORBIDDEN(403, "Forbidden"),
1365 NOT_FOUND(404, "Not Found"),
1366 METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
1367 NOT_ACCEPTABLE(406, "Not Acceptable"),
1368 REQUEST_TIMEOUT(408, "Request Timeout"),
1369 CONFLICT(409, "Conflict"),
1371 LENGTH_REQUIRED(411, "Length Required"),
1372 PRECONDITION_FAILED(412, "Precondition Failed"),
1373 PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
1374 UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
1375 RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
1376 EXPECTATION_FAILED(417, "Expectation Failed"),
1377 TOO_MANY_REQUESTS(429, "Too Many Requests"),
1379 INTERNAL_ERROR(500, "Internal Server Error"),
1380 NOT_IMPLEMENTED(501, "Not Implemented"),
1381 SERVICE_UNAVAILABLE(503, "Service Unavailable"),
1382 UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
1384 private final int requestStatus
;
1386 private final String description
;
1388 Status(int requestStatus
, String description
) {
1389 this.requestStatus
= requestStatus
;
1390 this.description
= description
;
1393 public static Status
lookup(int requestStatus
) {
1394 for (Status status
: Status
.values()) {
1395 if (status
.getRequestStatus() == requestStatus
) {
1403 public String
getDescription() {
1404 return "" + this.requestStatus
+ " " + this.description
;
1408 public int getRequestStatus() {
1409 return this.requestStatus
;
1415 * Output stream that will automatically send every write to the wrapped
1416 * OutputStream according to chunked transfer:
1417 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
1419 private static class ChunkedOutputStream
extends FilterOutputStream
{
1421 public ChunkedOutputStream(OutputStream out
) {
1426 public void write(int b
) throws IOException
{
1434 public void write(byte[] b
) throws IOException
{
1435 write(b
, 0, b
.length
);
1439 public void write(byte[] b
, int off
, int len
) throws IOException
{
1442 out
.write(String
.format("%x\r\n", len
).getBytes());
1443 out
.write(b
, off
, len
);
1444 out
.write("\r\n".getBytes());
1447 public void finish() throws IOException
{
1448 out
.write("0\r\n\r\n".getBytes());
1454 * HTTP status code after processing, e.g. "200 OK", Status.OK
1456 private IStatus status
;
1459 * MIME type of content, e.g. "text/html"
1461 private String mimeType
;
1464 * Data of the response, may be null.
1466 private InputStream data
;
1468 private long contentLength
;
1471 * Headers for the HTTP response. Use addHeader() to add lines. the
1472 * lowercase map is automatically kept up to date.
1474 @SuppressWarnings("serial")
1475 private final Map
<String
, String
> header
= new HashMap
<String
, String
>() {
1477 public String
put(String key
, String value
) {
1478 lowerCaseHeader
.put(key
== null ? key
: key
.toLowerCase(), value
);
1479 return super.put(key
, value
);
1484 * copy of the header map with all the keys lowercase for faster
1487 private final Map
<String
, String
> lowerCaseHeader
= new HashMap
<String
, String
>();
1490 * The request method that spawned this response.
1492 private Method requestMethod
;
1495 * Use chunkedTransfer
1497 private boolean chunkedTransfer
;
1499 private boolean encodeAsGzip
;
1501 private boolean keepAlive
;
1504 * Creates a fixed length response if totalBytes>=0, otherwise chunked.
1506 protected Response(IStatus status
, String mimeType
, InputStream data
, long totalBytes
) {
1507 this.status
= status
;
1508 this.mimeType
= mimeType
;
1510 this.data
= new ByteArrayInputStream(new byte[0]);
1511 this.contentLength
= 0L;
1514 this.contentLength
= totalBytes
;
1516 this.chunkedTransfer
= this.contentLength
< 0;
1521 public void close() throws IOException
{
1522 if (this.data
!= null) {
1528 * Adds given line to the header.
1530 public void addHeader(String name
, String value
) {
1531 this.header
.put(name
, value
);
1535 * Indicate to close the connection after the Response has been sent.
1538 * {@code true} to hint connection closing, {@code false} to
1539 * let connection be closed by client.
1541 public void closeConnection(boolean close
) {
1543 this.header
.put("connection", "close");
1545 this.header
.remove("connection");
1549 * @return {@code true} if connection is to be closed after this
1550 * Response has been sent.
1552 public boolean isCloseConnection() {
1553 return "close".equals(getHeader("connection"));
1556 public InputStream
getData() {
1560 public String
getHeader(String name
) {
1561 return this.lowerCaseHeader
.get(name
.toLowerCase());
1564 public String
getMimeType() {
1565 return this.mimeType
;
1568 public Method
getRequestMethod() {
1569 return this.requestMethod
;
1572 public IStatus
getStatus() {
1576 public void setGzipEncoding(boolean encodeAsGzip
) {
1577 this.encodeAsGzip
= encodeAsGzip
;
1580 public void setKeepAlive(boolean useKeepAlive
) {
1581 this.keepAlive
= useKeepAlive
;
1585 * Sends given response to the socket.
1587 protected void send(OutputStream outputStream
) {
1588 SimpleDateFormat gmtFrmt
= new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale
.US
);
1589 gmtFrmt
.setTimeZone(TimeZone
.getTimeZone("GMT"));
1592 if (this.status
== null) {
1593 throw new Error("sendResponse(): Status can't be null.");
1595 PrintWriter pw
= new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream
, new ContentType(this.mimeType
).getEncoding())), false);
1596 pw
.append("HTTP/1.1 ").append(this.status
.getDescription()).append(" \r\n");
1597 if (this.mimeType
!= null) {
1598 printHeader(pw
, "Content-Type", this.mimeType
);
1600 if (getHeader("date") == null) {
1601 printHeader(pw
, "Date", gmtFrmt
.format(new Date()));
1603 for (Entry
<String
, String
> entry
: this.header
.entrySet()) {
1604 printHeader(pw
, entry
.getKey(), entry
.getValue());
1606 if (getHeader("connection") == null) {
1607 printHeader(pw
, "Connection", (this.keepAlive ?
"keep-alive" : "close"));
1609 if (getHeader("content-length") != null) {
1610 encodeAsGzip
= false;
1613 printHeader(pw
, "Content-Encoding", "gzip");
1614 setChunkedTransfer(true);
1616 long pending
= this.data
!= null ?
this.contentLength
: 0;
1617 if (this.requestMethod
!= Method
.HEAD
&& this.chunkedTransfer
) {
1618 printHeader(pw
, "Transfer-Encoding", "chunked");
1619 } else if (!encodeAsGzip
) {
1620 pending
= sendContentLengthHeaderIfNotAlreadyPresent(pw
, pending
);
1624 sendBodyWithCorrectTransferAndEncoding(outputStream
, pending
);
1625 outputStream
.flush();
1626 safeClose(this.data
);
1627 } catch (IOException ioe
) {
1628 NanoHTTPD
.LOG
.log(Level
.SEVERE
, "Could not send response to the client", ioe
);
1632 @SuppressWarnings("static-method")
1633 protected void printHeader(PrintWriter pw
, String key
, String value
) {
1634 pw
.append(key
).append(": ").append(value
).append("\r\n");
1637 protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw
, long defaultSize
) {
1638 String contentLengthString
= getHeader("content-length");
1639 long size
= defaultSize
;
1640 if (contentLengthString
!= null) {
1642 size
= Long
.parseLong(contentLengthString
);
1643 } catch (NumberFormatException ex
) {
1644 LOG
.severe("content-length was no number " + contentLengthString
);
1647 pw
.print("Content-Length: " + size
+ "\r\n");
1651 private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream
, long pending
) throws IOException
{
1652 if (this.requestMethod
!= Method
.HEAD
&& this.chunkedTransfer
) {
1653 ChunkedOutputStream chunkedOutputStream
= new ChunkedOutputStream(outputStream
);
1654 sendBodyWithCorrectEncoding(chunkedOutputStream
, -1);
1655 chunkedOutputStream
.finish();
1657 sendBodyWithCorrectEncoding(outputStream
, pending
);
1661 private void sendBodyWithCorrectEncoding(OutputStream outputStream
, long pending
) throws IOException
{
1663 GZIPOutputStream gzipOutputStream
= new GZIPOutputStream(outputStream
);
1664 sendBody(gzipOutputStream
, -1);
1665 gzipOutputStream
.finish();
1667 sendBody(outputStream
, pending
);
1672 * Sends the body to the specified OutputStream. The pending parameter
1673 * limits the maximum amounts of bytes sent unless it is -1, in which
1674 * case everything is sent.
1676 * @param outputStream
1677 * the OutputStream to send data to
1679 * -1 to send everything, otherwise sets a max limit to the
1680 * number of bytes sent
1681 * @throws IOException
1682 * if something goes wrong while sending the data.
1684 private void sendBody(OutputStream outputStream
, long pending
) throws IOException
{
1685 long BUFFER_SIZE
= 16 * 1024;
1686 byte[] buff
= new byte[(int) BUFFER_SIZE
];
1687 boolean sendEverything
= pending
== -1;
1688 while (pending
> 0 || sendEverything
) {
1689 long bytesToRead
= sendEverything ? BUFFER_SIZE
: Math
.min(pending
, BUFFER_SIZE
);
1690 int read
= this.data
.read(buff
, 0, (int) bytesToRead
);
1694 outputStream
.write(buff
, 0, read
);
1695 if (!sendEverything
) {
1701 public void setChunkedTransfer(boolean chunkedTransfer
) {
1702 this.chunkedTransfer
= chunkedTransfer
;
1705 public void setData(InputStream data
) {
1709 public void setMimeType(String mimeType
) {
1710 this.mimeType
= mimeType
;
1713 public void setRequestMethod(Method requestMethod
) {
1714 this.requestMethod
= requestMethod
;
1717 public void setStatus(IStatus status
) {
1718 this.status
= status
;
1722 public static final class ResponseException
extends Exception
{
1724 private static final long serialVersionUID
= 6569838532917408380L;
1726 private final Response
.Status status
;
1728 public ResponseException(Response
.Status status
, String message
) {
1730 this.status
= status
;
1733 public ResponseException(Response
.Status status
, String message
, Exception e
) {
1735 this.status
= status
;
1738 public Response
.Status
getStatus() {
1744 * The runnable that will be used for the main listening thread.
1746 public class ServerRunnable
implements Runnable
{
1748 private final int timeout
;
1750 private IOException bindException
;
1752 private boolean hasBinded
= false;
1754 public ServerRunnable(int timeout
) {
1755 this.timeout
= timeout
;
1761 myServerSocket
.bind(hostname
!= null ?
new InetSocketAddress(hostname
, myPort
) : new InetSocketAddress(myPort
));
1763 } catch (IOException e
) {
1764 this.bindException
= e
;
1769 final Socket finalAccept
= NanoHTTPD
.this.myServerSocket
.accept();
1770 if (this.timeout
> 0) {
1771 finalAccept
.setSoTimeout(this.timeout
);
1773 final InputStream inputStream
= finalAccept
.getInputStream();
1774 NanoHTTPD
.this.asyncRunner
.exec(createClientHandler(finalAccept
, inputStream
));
1775 } catch (IOException e
) {
1776 NanoHTTPD
.LOG
.log(Level
.FINE
, "Communication with the client broken", e
);
1778 } while (!NanoHTTPD
.this.myServerSocket
.isClosed());
1786 * Temp files are responsible for managing the actual temporary storage and
1787 * cleaning themselves up when no longer needed.
1790 public interface TempFile
{
1792 public void delete() throws Exception
;
1794 public String
getName();
1796 public OutputStream
open() throws Exception
;
1800 * Temp file manager.
1803 * Temp file managers are created 1-to-1 with incoming requests, to create
1804 * and cleanup temporary files created as a result of handling the request.
1807 public interface TempFileManager
{
1811 public TempFile
createTempFile(String filename_hint
) throws Exception
;
1815 * Factory to create temp file managers.
1817 public interface TempFileManagerFactory
{
1819 public TempFileManager
create();
1823 * Factory to create ServerSocketFactories.
1825 public interface ServerSocketFactory
{
1827 public ServerSocket
create() throws IOException
;
1832 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
1833 * This is required as the Keep-Alive HTTP connections would otherwise block
1834 * the socket reading thread forever (or as long the browser is open).
1836 public static final int SOCKET_READ_TIMEOUT
= 5000;
1839 * Common MIME type for dynamic content: plain text
1841 public static final String MIME_PLAINTEXT
= "text/plain";
1844 * Common MIME type for dynamic content: html
1846 public static final String MIME_HTML
= "text/html";
1849 * Pseudo-Parameter to use to store the actual query string in the
1850 * parameters map for later re-processing.
1852 private static final String QUERY_STRING_PARAMETER
= "NanoHttpd.QUERY_STRING";
1857 private static final Logger LOG
= Logger
.getLogger(NanoHTTPD
.class.getName());
1860 * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
1862 protected static Map
<String
, String
> MIME_TYPES
;
1864 public static Map
<String
, String
> mimeTypes() {
1865 if (MIME_TYPES
== null) {
1866 MIME_TYPES
= new HashMap
<String
, String
>();
1867 loadMimeTypes(MIME_TYPES
, "META-INF/nanohttpd/default-mimetypes.properties");
1868 loadMimeTypes(MIME_TYPES
, "META-INF/nanohttpd/mimetypes.properties");
1869 if (MIME_TYPES
.isEmpty()) {
1870 LOG
.log(Level
.WARNING
, "no mime types found in the classpath! please provide mimetypes.properties");
1880 private static void loadMimeTypes(Map
<String
, String
> result
, String resourceName
) {
1882 Enumeration
<URL
> resources
= NanoHTTPD
.class.getClassLoader().getResources(resourceName
);
1883 while (resources
.hasMoreElements()) {
1884 URL url
= (URL
) resources
.nextElement();
1885 Properties properties
= new Properties();
1886 InputStream stream
= null;
1888 stream
= url
.openStream();
1889 properties
.load(stream
);
1890 } catch (IOException e
) {
1891 LOG
.log(Level
.SEVERE
, "could not load mimetypes from " + url
, e
);
1895 result
.putAll((Map
) properties
);
1897 } catch (IOException e
) {
1898 LOG
.log(Level
.INFO
, "no mime types available at " + resourceName
);
1903 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
1904 * array of loaded KeyManagers. These objects must properly
1905 * loaded/initialized by the caller.
1907 public static SSLServerSocketFactory
makeSSLSocketFactory(KeyStore loadedKeyStore
, KeyManager
[] keyManagers
) throws IOException
{
1908 SSLServerSocketFactory res
= null;
1910 TrustManagerFactory trustManagerFactory
= TrustManagerFactory
.getInstance(TrustManagerFactory
.getDefaultAlgorithm());
1911 trustManagerFactory
.init(loadedKeyStore
);
1912 SSLContext ctx
= SSLContext
.getInstance("TLS");
1913 ctx
.init(keyManagers
, trustManagerFactory
.getTrustManagers(), null);
1914 res
= ctx
.getServerSocketFactory();
1915 } catch (Exception e
) {
1916 throw new IOException(e
.getMessage());
1922 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
1923 * loaded KeyManagerFactory. These objects must properly loaded/initialized
1926 public static SSLServerSocketFactory
makeSSLSocketFactory(KeyStore loadedKeyStore
, KeyManagerFactory loadedKeyFactory
) throws IOException
{
1928 return makeSSLSocketFactory(loadedKeyStore
, loadedKeyFactory
.getKeyManagers());
1929 } catch (Exception e
) {
1930 throw new IOException(e
.getMessage());
1935 * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
1936 * certificate and passphrase
1938 public static SSLServerSocketFactory
makeSSLSocketFactory(String keyAndTrustStoreClasspathPath
, char[] passphrase
) throws IOException
{
1940 KeyStore keystore
= KeyStore
.getInstance(KeyStore
.getDefaultType());
1941 InputStream keystoreStream
= NanoHTTPD
.class.getResourceAsStream(keyAndTrustStoreClasspathPath
);
1943 if (keystoreStream
== null) {
1944 throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath
);
1947 keystore
.load(keystoreStream
, passphrase
);
1948 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
1949 keyManagerFactory
.init(keystore
, passphrase
);
1950 return makeSSLSocketFactory(keystore
, keyManagerFactory
);
1951 } catch (Exception e
) {
1952 throw new IOException(e
.getMessage());
1957 * Get MIME type from file name extension, if possible
1960 * the string representing a file
1961 * @return the connected mime/type
1963 public static String
getMimeTypeForFile(String uri
) {
1964 int dot
= uri
.lastIndexOf('.');
1967 mime
= mimeTypes().get(uri
.substring(dot
+ 1).toLowerCase());
1969 return mime
== null ?
"application/octet-stream" : mime
;
1972 private static final void safeClose(Object closeable
) {
1974 if (closeable
!= null) {
1975 if (closeable
instanceof Closeable
) {
1976 ((Closeable
) closeable
).close();
1977 } else if (closeable
instanceof Socket
) {
1978 ((Socket
) closeable
).close();
1979 } else if (closeable
instanceof ServerSocket
) {
1980 ((ServerSocket
) closeable
).close();
1982 throw new IllegalArgumentException("Unknown object to close");
1985 } catch (IOException e
) {
1986 NanoHTTPD
.LOG
.log(Level
.SEVERE
, "Could not close", e
);
1990 private final String hostname
;
1992 private final int myPort
;
1994 private volatile ServerSocket myServerSocket
;
1996 private ServerSocketFactory serverSocketFactory
= new DefaultServerSocketFactory();
1998 private Thread myThread
;
2001 * Pluggable strategy for asynchronously executing requests.
2003 protected AsyncRunner asyncRunner
;
2006 * Pluggable strategy for creating and cleaning up temporary files.
2008 private TempFileManagerFactory tempFileManagerFactory
;
2011 * Constructs an HTTP server on given port.
2013 public NanoHTTPD(int port
) {
2017 // -------------------------------------------------------------------------------
2020 // Threading Strategy.
2022 // -------------------------------------------------------------------------------
2026 * Constructs an HTTP server on given hostname and port.
2028 public NanoHTTPD(String hostname
, int port
) {
2029 this.hostname
= hostname
;
2031 setTempFileManagerFactory(new DefaultTempFileManagerFactory());
2032 setAsyncRunner(new DefaultAsyncRunner());
2036 * Forcibly closes all connections that are open.
2038 public synchronized void closeAllConnections() {
2043 * create a instance of the client handler, subclasses can return a subclass
2044 * of the ClientHandler.
2046 * @param finalAccept
2047 * the socket the cleint is connected to
2048 * @param inputStream
2050 * @return the client handler
2052 protected ClientHandler
createClientHandler(final Socket finalAccept
, final InputStream inputStream
) {
2053 return new ClientHandler(inputStream
, finalAccept
);
2057 * Instantiate the server runnable, can be overwritten by subclasses to
2058 * provide a subclass of the ServerRunnable.
2061 * the socet timeout to use.
2062 * @return the server runnable.
2064 protected ServerRunnable
createServerRunnable(final int timeout
) {
2065 return new ServerRunnable(timeout
);
2069 * Decode parameters from a URL, handing the case where a single parameter
2070 * name might have been supplied several times, by return lists of values.
2071 * In general these lists will contain a single element.
2074 * original <b>NanoHTTPD</b> parameters values, as passed to the
2075 * <code>serve()</code> method.
2076 * @return a map of <code>String</code> (parameter name) to
2077 * <code>List<String></code> (a list of the values supplied).
2079 protected static Map
<String
, List
<String
>> decodeParameters(Map
<String
, String
> parms
) {
2080 return decodeParameters(parms
.get(NanoHTTPD
.QUERY_STRING_PARAMETER
));
2083 // -------------------------------------------------------------------------------
2087 * Decode parameters from a URL, handing the case where a single parameter
2088 * name might have been supplied several times, by return lists of values.
2089 * In general these lists will contain a single element.
2091 * @param queryString
2092 * a query string pulled from the URL.
2093 * @return a map of <code>String</code> (parameter name) to
2094 * <code>List<String></code> (a list of the values supplied).
2096 protected static Map
<String
, List
<String
>> decodeParameters(String queryString
) {
2097 Map
<String
, List
<String
>> parms
= new HashMap
<String
, List
<String
>>();
2098 if (queryString
!= null) {
2099 StringTokenizer st
= new StringTokenizer(queryString
, "&");
2100 while (st
.hasMoreTokens()) {
2101 String e
= st
.nextToken();
2102 int sep
= e
.indexOf('=');
2103 String propertyName
= sep
>= 0 ?
decodePercent(e
.substring(0, sep
)).trim() : decodePercent(e
).trim();
2104 if (!parms
.containsKey(propertyName
)) {
2105 parms
.put(propertyName
, new ArrayList
<String
>());
2107 String propertyValue
= sep
>= 0 ?
decodePercent(e
.substring(sep
+ 1)) : null;
2108 if (propertyValue
!= null) {
2109 parms
.get(propertyName
).add(propertyValue
);
2117 * Decode percent encoded <code>String</code> values.
2120 * the percent encoded <code>String</code>
2121 * @return expanded form of the input, for example "foo%20bar" becomes
2124 protected static String
decodePercent(String str
) {
2125 String decoded
= null;
2127 decoded
= URLDecoder
.decode(str
, "UTF8");
2128 } catch (UnsupportedEncodingException ignored
) {
2129 NanoHTTPD
.LOG
.log(Level
.WARNING
, "Encoding not supported, ignored", ignored
);
2135 * @return true if the gzip compression should be used if the client
2136 * accespts it. Default this option is on for text content and off
2137 * for everything. Override this for custom semantics.
2139 @SuppressWarnings("static-method")
2140 protected boolean useGzipWhenAccepted(Response r
) {
2141 return r
.getMimeType() != null && (r
.getMimeType().toLowerCase().contains("text/") || r
.getMimeType().toLowerCase().contains("/json"));
2144 public final int getListeningPort() {
2145 return this.myServerSocket
== null ?
-1 : this.myServerSocket
.getLocalPort();
2148 public final boolean isAlive() {
2149 return wasStarted() && !this.myServerSocket
.isClosed() && this.myThread
.isAlive();
2152 public ServerSocketFactory
getServerSocketFactory() {
2153 return serverSocketFactory
;
2156 public void setServerSocketFactory(ServerSocketFactory serverSocketFactory
) {
2157 this.serverSocketFactory
= serverSocketFactory
;
2160 public String
getHostname() {
2164 public TempFileManagerFactory
getTempFileManagerFactory() {
2165 return tempFileManagerFactory
;
2169 * Call before start() to serve over HTTPS instead of HTTP
2171 public void makeSecure(SSLServerSocketFactory sslServerSocketFactory
, String
[] sslProtocols
) {
2172 this.serverSocketFactory
= new SecureServerSocketFactory(sslServerSocketFactory
, sslProtocols
);
2176 * Create a response with unknown length (using HTTP 1.1 chunking).
2178 public static Response
newChunkedResponse(IStatus status
, String mimeType
, InputStream data
) {
2179 return new Response(status
, mimeType
, data
, -1);
2183 * Create a response with known length.
2185 public static Response
newFixedLengthResponse(IStatus status
, String mimeType
, InputStream data
, long totalBytes
) {
2186 return new Response(status
, mimeType
, data
, totalBytes
);
2190 * Create a text response with known length.
2192 public static Response
newFixedLengthResponse(IStatus status
, String mimeType
, String txt
) {
2193 ContentType contentType
= new ContentType(mimeType
);
2195 return newFixedLengthResponse(status
, mimeType
, new ByteArrayInputStream(new byte[0]), 0);
2199 CharsetEncoder newEncoder
= Charset
.forName(contentType
.getEncoding()).newEncoder();
2200 if (!newEncoder
.canEncode(txt
)) {
2201 contentType
= contentType
.tryUTF8();
2203 bytes
= txt
.getBytes(contentType
.getEncoding());
2204 } catch (UnsupportedEncodingException e
) {
2205 NanoHTTPD
.LOG
.log(Level
.SEVERE
, "encoding problem, responding nothing", e
);
2206 bytes
= new byte[0];
2208 return newFixedLengthResponse(status
, contentType
.getContentTypeHeader(), new ByteArrayInputStream(bytes
), bytes
.length
);
2213 * Create a text response with known length.
2215 public static Response
newFixedLengthResponse(String msg
) {
2216 return newFixedLengthResponse(Status
.OK
, NanoHTTPD
.MIME_HTML
, msg
);
2220 * Override this to customize the server.
2223 * (By default, this returns a 404 "Not Found" plain text error response.)
2227 * @return HTTP response, see class Response for details
2229 public Response
serve(IHTTPSession session
) {
2230 Map
<String
, String
> files
= new HashMap
<String
, String
>();
2231 Method method
= session
.getMethod();
2232 if (Method
.PUT
.equals(method
) || Method
.POST
.equals(method
)) {
2234 session
.parseBody(files
);
2235 } catch (IOException ioe
) {
2236 return newFixedLengthResponse(Response
.Status
.INTERNAL_ERROR
, NanoHTTPD
.MIME_PLAINTEXT
, "SERVER INTERNAL ERROR: IOException: " + ioe
.getMessage());
2237 } catch (ResponseException re
) {
2238 return newFixedLengthResponse(re
.getStatus(), NanoHTTPD
.MIME_PLAINTEXT
, re
.getMessage());
2242 Map
<String
, String
> parms
= session
.getParms();
2243 parms
.put(NanoHTTPD
.QUERY_STRING_PARAMETER
, session
.getQueryParameterString());
2244 return serve(session
.getUri(), method
, session
.getHeaders(), parms
, files
);
2248 * Override this to customize the server.
2251 * (By default, this returns a 404 "Not Found" plain text error response.)
2254 * Percent-decoded URI without parameters, for example
2257 * "GET", "POST" etc.
2259 * Parsed, percent decoded parameters from URI and, in case of
2262 * Header entries, percent decoded
2263 * @return HTTP response, see class Response for details
2266 public Response
serve(String uri
, Method method
, Map
<String
, String
> headers
, Map
<String
, String
> parms
, Map
<String
, String
> files
) {
2267 return newFixedLengthResponse(Response
.Status
.NOT_FOUND
, NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
2271 * Pluggable strategy for asynchronously executing requests.
2273 * @param asyncRunner
2274 * new strategy for handling threads.
2276 public void setAsyncRunner(AsyncRunner asyncRunner
) {
2277 this.asyncRunner
= asyncRunner
;
2281 * Pluggable strategy for creating and cleaning up temporary files.
2283 * @param tempFileManagerFactory
2284 * new strategy for handling temp files.
2286 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory
) {
2287 this.tempFileManagerFactory
= tempFileManagerFactory
;
2293 * @throws IOException
2294 * if the socket is in use.
2296 public void start() throws IOException
{
2297 start(NanoHTTPD
.SOCKET_READ_TIMEOUT
);
2301 * Starts the server (in setDaemon(true) mode).
2303 public void start(final int timeout
) throws IOException
{
2304 start(timeout
, true);
2311 * timeout to use for socket connections.
2313 * start the thread daemon or not.
2314 * @throws IOException
2315 * if the socket is in use.
2317 public void start(final int timeout
, boolean daemon
) throws IOException
{
2318 this.myServerSocket
= this.getServerSocketFactory().create();
2319 this.myServerSocket
.setReuseAddress(true);
2321 ServerRunnable serverRunnable
= createServerRunnable(timeout
);
2322 this.myThread
= new Thread(serverRunnable
);
2323 this.myThread
.setDaemon(daemon
);
2324 this.myThread
.setName("NanoHttpd Main Listener");
2325 this.myThread
.start();
2326 while (!serverRunnable
.hasBinded
&& serverRunnable
.bindException
== null) {
2329 } catch (Throwable e
) {
2330 // on android this may not be allowed, that's why we
2331 // catch throwable the wait should be very short because we are
2332 // just waiting for the bind of the socket
2335 if (serverRunnable
.bindException
!= null) {
2336 throw serverRunnable
.bindException
;
2343 public void stop() {
2345 safeClose(this.myServerSocket
);
2346 this.asyncRunner
.closeAll();
2347 if (this.myThread
!= null) {
2348 this.myThread
.join();
2350 } catch (Exception e
) {
2351 NanoHTTPD
.LOG
.log(Level
.SEVERE
, "Could not stop all connections", e
);
2355 public final boolean wasStarted() {
2356 return this.myServerSocket
!= null && this.myThread
!= null;