• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package fi.iki.elonen;
2 
3 import java.io.*;
4 import java.net.InetAddress;
5 import java.net.InetSocketAddress;
6 import java.net.ServerSocket;
7 import java.net.Socket;
8 import java.net.SocketException;
9 import java.net.SocketTimeoutException;
10 import java.net.URLDecoder;
11 import java.nio.ByteBuffer;
12 import java.nio.channels.FileChannel;
13 import java.text.SimpleDateFormat;
14 import java.util.ArrayList;
15 import java.util.Calendar;
16 import java.util.Date;
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.StringTokenizer;
25 import java.util.TimeZone;
26 
27 /**
28  * A simple, tiny, nicely embeddable HTTP server in Java
29  * <p/>
30  * <p/>
31  * NanoHTTPD
32  * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p>
33  * <p/>
34  * <p/>
35  * <b>Features + limitations: </b>
36  * <ul>
37  * <p/>
38  * <li>Only one Java file</li>
39  * <li>Java 5 compatible</li>
40  * <li>Released as open source, Modified BSD licence</li>
41  * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
42  * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
43  * <li>Supports both dynamic content and file serving</li>
44  * <li>Supports file upload (since version 1.2, 2010)</li>
45  * <li>Supports partial content (streaming)</li>
46  * <li>Supports ETags</li>
47  * <li>Never caches anything</li>
48  * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
49  * <li>Default code serves files and shows all HTTP parameters and headers</li>
50  * <li>File server supports directory listing, index.html and index.htm</li>
51  * <li>File server supports partial content (streaming)</li>
52  * <li>File server supports ETags</li>
53  * <li>File server does the 301 redirection trick for directories without '/'</li>
54  * <li>File server supports simple skipping for files (continue download)</li>
55  * <li>File server serves also very long files without memory overhead</li>
56  * <li>Contains a built-in list of most common mime types</li>
57  * <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
58  * <p/>
59  * </ul>
60  * <p/>
61  * <p/>
62  * <b>How to use: </b>
63  * <ul>
64  * <p/>
65  * <li>Subclass and implement serve() and embed to your own program</li>
66  * <p/>
67  * </ul>
68  * <p/>
69  * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence)
70  */
71 public abstract class NanoHTTPD {
72     /**
73      * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
74      * This is required as the Keep-Alive HTTP connections would otherwise
75      * block the socket reading thread forever (or as long the browser is open).
76      */
77     public static final int SOCKET_READ_TIMEOUT = 5000;
78     /**
79      * Common mime type for dynamic content: plain text
80      */
81     public static final String MIME_PLAINTEXT = "text/plain";
82     /**
83      * Common mime type for dynamic content: html
84      */
85     public static final String MIME_HTML = "text/html";
86     /**
87      * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing.
88      */
89     private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
90     private final String hostname;
91     private final int myPort;
92     private ServerSocket myServerSocket;
93     private Set<Socket> openConnections = new HashSet<Socket>();
94     private Thread myThread;
95     /**
96      * Pluggable strategy for asynchronously executing requests.
97      */
98     private AsyncRunner asyncRunner;
99     /**
100      * Pluggable strategy for creating and cleaning up temporary files.
101      */
102     private TempFileManagerFactory tempFileManagerFactory;
103 
104     /**
105      * Constructs an HTTP server on given port.
106      */
NanoHTTPD(int port)107     public NanoHTTPD(int port) {
108         this(null, port);
109     }
110 
111     /**
112      * Constructs an HTTP server on given hostname and port.
113      */
NanoHTTPD(String hostname, int port)114     public NanoHTTPD(String hostname, int port) {
115         this.hostname = hostname;
116         this.myPort = port;
117         setTempFileManagerFactory(new DefaultTempFileManagerFactory());
118         setAsyncRunner(new DefaultAsyncRunner());
119     }
120 
safeClose(Closeable closeable)121     private static final void safeClose(Closeable closeable) {
122         if (closeable != null) {
123             try {
124                 closeable.close();
125             } catch (IOException e) {
126             }
127         }
128     }
129 
safeClose(Socket closeable)130     private static final void safeClose(Socket closeable) {
131         if (closeable != null) {
132             try {
133                 closeable.close();
134             } catch (IOException e) {
135             }
136         }
137     }
138 
safeClose(ServerSocket closeable)139     private static final void safeClose(ServerSocket closeable) {
140         if (closeable != null) {
141             try {
142                 closeable.close();
143             } catch (IOException e) {
144             }
145         }
146     }
147 
148     /**
149      * Start the server.
150      *
151      * @throws IOException if the socket is in use.
152      */
start()153     public void start() throws IOException {
154         myServerSocket = new ServerSocket();
155         myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
156 
157         myThread = new Thread(new Runnable() {
158             @Override
159             public void run() {
160                 do {
161                     try {
162                         final Socket finalAccept = myServerSocket.accept();
163                         registerConnection(finalAccept);
164                         finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
165                         final InputStream inputStream = finalAccept.getInputStream();
166                         asyncRunner.exec(new Runnable() {
167                             @Override
168                             public void run() {
169                                 OutputStream outputStream = null;
170                                 try {
171                                     outputStream = finalAccept.getOutputStream();
172                                     TempFileManager tempFileManager = tempFileManagerFactory.create();
173                                     HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
174                                     while (!finalAccept.isClosed()) {
175                                         session.execute();
176                                     }
177                                 } catch (Exception e) {
178                                     // When the socket is closed by the client, we throw our own SocketException
179                                     // to break the  "keep alive" loop above.
180                                     if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {
181                                         e.printStackTrace();
182                                     }
183                                 } finally {
184                                     safeClose(outputStream);
185                                     safeClose(inputStream);
186                                     safeClose(finalAccept);
187                                     unRegisterConnection(finalAccept);
188                                 }
189                             }
190                         });
191                     } catch (IOException e) {
192                     }
193                 } while (!myServerSocket.isClosed());
194             }
195         });
196         myThread.setDaemon(true);
197         myThread.setName("NanoHttpd Main Listener");
198         myThread.start();
199     }
200 
201     /**
202      * Stop the server.
203      */
stop()204     public void stop() {
205         try {
206             safeClose(myServerSocket);
207             closeAllConnections();
208             if (myThread != null) {
209                 myThread.join();
210             }
211         } catch (Exception e) {
212             e.printStackTrace();
213         }
214     }
215 
216     /**
217      * Registers that a new connection has been set up.
218      *
219      * @param socket the {@link Socket} for the connection.
220      */
registerConnection(Socket socket)221     public synchronized void registerConnection(Socket socket) {
222         openConnections.add(socket);
223     }
224 
225     /**
226      * Registers that a connection has been closed
227      *
228      * @param socket
229      *            the {@link Socket} for the connection.
230      */
unRegisterConnection(Socket socket)231     public synchronized void unRegisterConnection(Socket socket) {
232         openConnections.remove(socket);
233     }
234 
235     /**
236      * Forcibly closes all connections that are open.
237      */
closeAllConnections()238     public synchronized void closeAllConnections() {
239         for (Socket socket : openConnections) {
240             safeClose(socket);
241         }
242     }
243 
getListeningPort()244     public final int getListeningPort() {
245         return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
246     }
247 
wasStarted()248     public final boolean wasStarted() {
249         return myServerSocket != null && myThread != null;
250     }
251 
isAlive()252     public final boolean isAlive() {
253         return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive();
254     }
255 
256     /**
257      * Override this to customize the server.
258      * <p/>
259      * <p/>
260      * (By default, this delegates to serveFile() and allows directory listing.)
261      *
262      * @param uri     Percent-decoded URI without parameters, for example "/index.cgi"
263      * @param method  "GET", "POST" etc.
264      * @param parms   Parsed, percent decoded parameters from URI and, in case of POST, data.
265      * @param headers Header entries, percent decoded
266      * @return HTTP response, see class Response for details
267      */
268     @Deprecated
serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)269     public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms,
270                                    Map<String, String> files) {
271         return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
272     }
273 
274     /**
275      * Override this to customize the server.
276      * <p/>
277      * <p/>
278      * (By default, this delegates to serveFile() and allows directory listing.)
279      *
280      * @param session The HTTP session
281      * @return HTTP response, see class Response for details
282      */
serve(IHTTPSession session)283     public Response serve(IHTTPSession session) {
284         Map<String, String> files = new HashMap<String, String>();
285         Method method = session.getMethod();
286         if (Method.PUT.equals(method) || Method.POST.equals(method)) {
287             try {
288                 session.parseBody(files);
289             } catch (IOException ioe) {
290                 return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
291             } catch (ResponseException re) {
292                 return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
293             }
294         }
295 
296         Map<String, String> parms = session.getParms();
297         parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
298         return serve(session.getUri(), method, session.getHeaders(), parms, files);
299     }
300 
301     /**
302      * Decode percent encoded <code>String</code> values.
303      *
304      * @param str the percent encoded <code>String</code>
305      * @return expanded form of the input, for example "foo%20bar" becomes "foo bar"
306      */
decodePercent(String str)307     protected String decodePercent(String str) {
308         String decoded = null;
309         try {
310             decoded = URLDecoder.decode(str, "UTF8");
311         } catch (UnsupportedEncodingException ignored) {
312         }
313         return decoded;
314     }
315 
316     /**
317      * Decode parameters from a URL, handing the case where a single parameter name might have been
318      * supplied several times, by return lists of values.  In general these lists will contain a single
319      * element.
320      *
321      * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method.
322      * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
323      */
decodeParameters(Map<String, String> parms)324     protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
325         return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
326     }
327 
328     /**
329      * Decode parameters from a URL, handing the case where a single parameter name might have been
330      * supplied several times, by return lists of values.  In general these lists will contain a single
331      * element.
332      *
333      * @param queryString a query string pulled from the URL.
334      * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
335      */
decodeParameters(String queryString)336     protected Map<String, List<String>> decodeParameters(String queryString) {
337         Map<String, List<String>> parms = new HashMap<String, List<String>>();
338         if (queryString != null) {
339             StringTokenizer st = new StringTokenizer(queryString, "&");
340             while (st.hasMoreTokens()) {
341                 String e = st.nextToken();
342                 int sep = e.indexOf('=');
343                 String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
344                 if (!parms.containsKey(propertyName)) {
345                     parms.put(propertyName, new ArrayList<String>());
346                 }
347                 String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
348                 if (propertyValue != null) {
349                     parms.get(propertyName).add(propertyValue);
350                 }
351             }
352         }
353         return parms;
354     }
355 
356     // ------------------------------------------------------------------------------- //
357     //
358     // Threading Strategy.
359     //
360     // ------------------------------------------------------------------------------- //
361 
362     /**
363      * Pluggable strategy for asynchronously executing requests.
364      *
365      * @param asyncRunner new strategy for handling threads.
366      */
setAsyncRunner(AsyncRunner asyncRunner)367     public void setAsyncRunner(AsyncRunner asyncRunner) {
368         this.asyncRunner = asyncRunner;
369     }
370 
371     // ------------------------------------------------------------------------------- //
372     //
373     // Temp file handling strategy.
374     //
375     // ------------------------------------------------------------------------------- //
376 
377     /**
378      * Pluggable strategy for creating and cleaning up temporary files.
379      *
380      * @param tempFileManagerFactory new strategy for handling temp files.
381      */
setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory)382     public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
383         this.tempFileManagerFactory = tempFileManagerFactory;
384     }
385 
386     /**
387      * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value.
388      */
389     public enum Method {
390         GET, PUT, POST, DELETE, HEAD, OPTIONS;
391 
lookup(String method)392         static Method lookup(String method) {
393             for (Method m : Method.values()) {
394                 if (m.toString().equalsIgnoreCase(method)) {
395                     return m;
396                 }
397             }
398             return null;
399         }
400     }
401 
402     /**
403      * Pluggable strategy for asynchronously executing requests.
404      */
405     public interface AsyncRunner {
exec(Runnable code)406         void exec(Runnable code);
407     }
408 
409     /**
410      * Factory to create temp file managers.
411      */
412     public interface TempFileManagerFactory {
create()413         TempFileManager create();
414     }
415 
416     // ------------------------------------------------------------------------------- //
417 
418     /**
419      * Temp file manager.
420      * <p/>
421      * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup
422      * temporary files created as a result of handling the request.</p>
423      */
424     public interface TempFileManager {
createTempFile()425         TempFile createTempFile() throws Exception;
426 
clear()427         void clear();
428     }
429 
430     /**
431      * A temp file.
432      * <p/>
433      * <p>Temp files are responsible for managing the actual temporary storage and cleaning
434      * themselves up when no longer needed.</p>
435      */
436     public interface TempFile {
open()437         OutputStream open() throws Exception;
438 
delete()439         void delete() throws Exception;
440 
getName()441         String getName();
442     }
443 
444     /**
445      * Default threading strategy for NanoHttpd.
446      * <p/>
447      * <p>By default, the server spawns a new Thread for every incoming request.  These are set
448      * to <i>daemon</i> status, and named according to the request number.  The name is
449      * useful when profiling the application.</p>
450      */
451     public static class DefaultAsyncRunner implements AsyncRunner {
452         private long requestCount;
453 
454         @Override
exec(Runnable code)455         public void exec(Runnable code) {
456             ++requestCount;
457             Thread t = new Thread(code);
458             t.setDaemon(true);
459             t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
460             t.start();
461         }
462     }
463 
464     /**
465      * Default strategy for creating and cleaning up temporary files.
466      * <p/>
467      * <p></p>This class stores its files in the standard location (that is,
468      * wherever <code>java.io.tmpdir</code> points to).  Files are added
469      * to an internal list, and deleted when no longer needed (that is,
470      * when <code>clear()</code> is invoked at the end of processing a
471      * request).</p>
472      */
473     public static class DefaultTempFileManager implements TempFileManager {
474         private final String tmpdir;
475         private final List<TempFile> tempFiles;
476 
DefaultTempFileManager()477         public DefaultTempFileManager() {
478             tmpdir = System.getProperty("java.io.tmpdir");
479             tempFiles = new ArrayList<TempFile>();
480         }
481 
482         @Override
createTempFile()483         public TempFile createTempFile() throws Exception {
484             DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
485             tempFiles.add(tempFile);
486             return tempFile;
487         }
488 
489         @Override
clear()490         public void clear() {
491             for (TempFile file : tempFiles) {
492                 try {
493                     file.delete();
494                 } catch (Exception ignored) {
495                 }
496             }
497             tempFiles.clear();
498         }
499     }
500 
501     /**
502      * Default strategy for creating and cleaning up temporary files.
503      * <p/>
504      * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in
505      * the directory specified.</p>
506      */
507     public static class DefaultTempFile implements TempFile {
508         private File file;
509         private OutputStream fstream;
510 
DefaultTempFile(String tempdir)511         public DefaultTempFile(String tempdir) throws IOException {
512             file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
513             fstream = new FileOutputStream(file);
514         }
515 
516         @Override
open()517         public OutputStream open() throws Exception {
518             return fstream;
519         }
520 
521         @Override
delete()522         public void delete() throws Exception {
523             safeClose(fstream);
524             file.delete();
525         }
526 
527         @Override
getName()528         public String getName() {
529             return file.getAbsolutePath();
530         }
531     }
532 
533     /**
534      * HTTP response. Return one of these from serve().
535      */
536     public static class Response {
537         /**
538          * HTTP status code after processing, e.g. "200 OK", HTTP_OK
539          */
540         private IStatus status;
541         /**
542          * MIME type of content, e.g. "text/html"
543          */
544         private String mimeType;
545         /**
546          * Data of the response, may be null.
547          */
548         private InputStream data;
549         /**
550          * Headers for the HTTP response. Use addHeader() to add lines.
551          */
552         private Map<String, String> header = new HashMap<String, String>();
553         /**
554          * The request method that spawned this response.
555          */
556         private Method requestMethod;
557         /**
558          * Use chunkedTransfer
559          */
560         private boolean chunkedTransfer;
561 
562         /**
563          * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
564          */
Response(String msg)565         public Response(String msg) {
566             this(Status.OK, MIME_HTML, msg);
567         }
568 
569         /**
570          * Basic constructor.
571          */
Response(IStatus status, String mimeType, InputStream data)572         public Response(IStatus status, String mimeType, InputStream data) {
573             this.status = status;
574             this.mimeType = mimeType;
575             this.data = data;
576         }
577 
578         /**
579          * Convenience method that makes an InputStream out of given text.
580          */
Response(IStatus status, String mimeType, String txt)581         public Response(IStatus status, String mimeType, String txt) {
582             this.status = status;
583             this.mimeType = mimeType;
584             try {
585                 this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
586             } catch (java.io.UnsupportedEncodingException uee) {
587                 uee.printStackTrace();
588             }
589         }
590 
591         /**
592          * Adds given line to the header.
593          */
addHeader(String name, String value)594         public void addHeader(String name, String value) {
595             header.put(name, value);
596         }
597 
getHeader(String name)598         public String getHeader(String name) {
599             return header.get(name);
600         }
601 
602         /**
603          * Sends given response to the socket.
604          */
send(OutputStream outputStream)605         protected void send(OutputStream outputStream) {
606             String mime = mimeType;
607             SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
608             gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
609 
610             try {
611                 if (status == null) {
612                     throw new Error("sendResponse(): Status can't be null.");
613                 }
614                 PrintWriter pw = new PrintWriter(outputStream);
615                 pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
616 
617                 if (mime != null) {
618                     pw.print("Content-Type: " + mime + "\r\n");
619                 }
620 
621                 if (header == null || header.get("Date") == null) {
622                     pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
623                 }
624 
625                 if (header != null) {
626                     for (String key : header.keySet()) {
627                         String value = header.get(key);
628                         pw.print(key + ": " + value + "\r\n");
629                     }
630                 }
631 
632                 sendConnectionHeaderIfNotAlreadyPresent(pw, header);
633 
634                 if (requestMethod != Method.HEAD && chunkedTransfer) {
635                     sendAsChunked(outputStream, pw);
636                 } else {
637                     int pending = data != null ? data.available() : 0;
638                     sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
639                     pw.print("\r\n");
640                     pw.flush();
641                     sendAsFixedLength(outputStream, pending);
642                 }
643                 outputStream.flush();
644                 safeClose(data);
645             } catch (IOException ioe) {
646                 // Couldn't write? No can do.
647             }
648         }
649 
sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size)650         protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) {
651             if (!headerAlreadySent(header, "content-length")) {
652                 pw.print("Content-Length: "+ size +"\r\n");
653             }
654         }
655 
sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header)656         protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
657             if (!headerAlreadySent(header, "connection")) {
658                 pw.print("Connection: keep-alive\r\n");
659             }
660         }
661 
headerAlreadySent(Map<String, String> header, String name)662         private boolean headerAlreadySent(Map<String, String> header, String name) {
663             boolean alreadySent = false;
664             for (String headerName : header.keySet()) {
665                 alreadySent |= headerName.equalsIgnoreCase(name);
666             }
667             return alreadySent;
668         }
669 
sendAsChunked(OutputStream outputStream, PrintWriter pw)670         private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
671             pw.print("Transfer-Encoding: chunked\r\n");
672             pw.print("\r\n");
673             pw.flush();
674             int BUFFER_SIZE = 16 * 1024;
675             byte[] CRLF = "\r\n".getBytes();
676             byte[] buff = new byte[BUFFER_SIZE];
677             int read;
678             while ((read = data.read(buff)) > 0) {
679                 outputStream.write(String.format("%x\r\n", read).getBytes());
680                 outputStream.write(buff, 0, read);
681                 outputStream.write(CRLF);
682             }
683             outputStream.write(String.format("0\r\n\r\n").getBytes());
684         }
685 
sendAsFixedLength(OutputStream outputStream, int pending)686         private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
687             if (requestMethod != Method.HEAD && data != null) {
688                 int BUFFER_SIZE = 16 * 1024;
689                 byte[] buff = new byte[BUFFER_SIZE];
690                 while (pending > 0) {
691                     int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
692                     if (read <= 0) {
693                         break;
694                     }
695                     outputStream.write(buff, 0, read);
696                     pending -= read;
697                 }
698             }
699         }
700 
getStatus()701         public IStatus getStatus() {
702             return status;
703         }
704 
setStatus(Status status)705         public void setStatus(Status status) {
706             this.status = status;
707         }
708 
getMimeType()709         public String getMimeType() {
710             return mimeType;
711         }
712 
setMimeType(String mimeType)713         public void setMimeType(String mimeType) {
714             this.mimeType = mimeType;
715         }
716 
getData()717         public InputStream getData() {
718             return data;
719         }
720 
setData(InputStream data)721         public void setData(InputStream data) {
722             this.data = data;
723         }
724 
getRequestMethod()725         public Method getRequestMethod() {
726             return requestMethod;
727         }
728 
setRequestMethod(Method requestMethod)729         public void setRequestMethod(Method requestMethod) {
730             this.requestMethod = requestMethod;
731         }
732 
setChunkedTransfer(boolean chunkedTransfer)733         public void setChunkedTransfer(boolean chunkedTransfer) {
734             this.chunkedTransfer = chunkedTransfer;
735         }
736 
737         public interface IStatus {
getRequestStatus()738             int getRequestStatus();
getDescription()739             String getDescription();
740         }
741 
742         /**
743          * Some HTTP response status codes
744          */
745         public enum Status implements IStatus {
746             SWITCH_PROTOCOL(101, "Switching Protocols"),
747 
748             OK(200, "OK"),
749             CREATED(201, "Created"),
750             ACCEPTED(202, "Accepted"),
751             NO_CONTENT(204, "No Content"),
752             PARTIAL_CONTENT(206, "Partial Content"),
753 
754             REDIRECT(301, "Moved Permanently"),
755             TEMPORARY_REDIRECT(302, "Moved Temporarily"),
756             NOT_MODIFIED(304, "Not Modified"),
757 
758             BAD_REQUEST(400, "Bad Request"),
759             UNAUTHORIZED(401, "Unauthorized"),
760             FORBIDDEN(403, "Forbidden"),
761             NOT_FOUND(404, "Not Found"),
762             METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
763             RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
764 
765             INTERNAL_ERROR(500, "Internal Server Error");
766 
767             private final int requestStatus;
768             private final String description;
769 
Status(int requestStatus, String description)770             Status(int requestStatus, String description) {
771                 this.requestStatus = requestStatus;
772                 this.description = description;
773             }
774 
775             @Override
getRequestStatus()776             public int getRequestStatus() {
777                 return this.requestStatus;
778             }
779 
780             @Override
getDescription()781             public String getDescription() {
782                 return "" + this.requestStatus + " " + description;
783             }
784         }
785     }
786 
787     public static final class ResponseException extends Exception {
788 
789         private final Response.Status status;
790 
ResponseException(Response.Status status, String message)791         public ResponseException(Response.Status status, String message) {
792             super(message);
793             this.status = status;
794         }
795 
ResponseException(Response.Status status, String message, Exception e)796         public ResponseException(Response.Status status, String message, Exception e) {
797             super(message, e);
798             this.status = status;
799         }
800 
getStatus()801         public Response.Status getStatus() {
802             return status;
803         }
804     }
805 
806     /**
807      * Default strategy for creating and cleaning up temporary files.
808      */
809     private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
810         @Override
create()811         public TempFileManager create() {
812             return new DefaultTempFileManager();
813         }
814     }
815 
816     /**
817      * Handles one session, i.e. parses the HTTP request and returns the response.
818      */
819     public interface IHTTPSession {
execute()820         void execute() throws IOException;
821 
getParms()822         Map<String, String> getParms();
823 
getHeaders()824         Map<String, String> getHeaders();
825 
826         /**
827          * @return the path part of the URL.
828          */
getUri()829         String getUri();
830 
getQueryParameterString()831         String getQueryParameterString();
832 
getMethod()833         Method getMethod();
834 
getInputStream()835         InputStream getInputStream();
836 
getCookies()837         CookieHandler getCookies();
838 
839         /**
840          * Adds the files in the request body to the files map.
841          * @arg files - map to modify
842          */
parseBody(Map<String, String> files)843         void parseBody(Map<String, String> files) throws IOException, ResponseException;
844     }
845 
846     protected class HTTPSession implements IHTTPSession {
847         public static final int BUFSIZE = 8192;
848         private final TempFileManager tempFileManager;
849         private final OutputStream outputStream;
850         private PushbackInputStream inputStream;
851         private int splitbyte;
852         private int rlen;
853         private String uri;
854         private Method method;
855         private Map<String, String> parms;
856         private Map<String, String> headers;
857         private CookieHandler cookies;
858         private String queryParameterString;
859 
HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream)860         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
861             this.tempFileManager = tempFileManager;
862             this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
863             this.outputStream = outputStream;
864         }
865 
HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress)866         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
867             this.tempFileManager = tempFileManager;
868             this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
869             this.outputStream = outputStream;
870             String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
871             headers = new HashMap<String, String>();
872 
873             headers.put("remote-addr", remoteIp);
874             headers.put("http-client-ip", remoteIp);
875         }
876 
877         @Override
execute()878         public void execute() throws IOException {
879             try {
880                 // Read the first 8192 bytes.
881                 // The full header should fit in here.
882                 // Apache's default header limit is 8KB.
883                 // Do NOT assume that a single read will get the entire header at once!
884                 byte[] buf = new byte[BUFSIZE];
885                 splitbyte = 0;
886                 rlen = 0;
887                 {
888                     int read = -1;
889                     try {
890                         read = inputStream.read(buf, 0, BUFSIZE);
891                     } catch (Exception e) {
892                         safeClose(inputStream);
893                         safeClose(outputStream);
894                         throw new SocketException("NanoHttpd Shutdown");
895                     }
896                     if (read == -1) {
897                         // socket was been closed
898                         safeClose(inputStream);
899                         safeClose(outputStream);
900                         throw new SocketException("NanoHttpd Shutdown");
901                     }
902                     while (read > 0) {
903                         rlen += read;
904                         splitbyte = findHeaderEnd(buf, rlen);
905                         if (splitbyte > 0)
906                             break;
907                         read = inputStream.read(buf, rlen, BUFSIZE - rlen);
908                     }
909                 }
910 
911                 if (splitbyte < rlen) {
912                     inputStream.unread(buf, splitbyte, rlen - splitbyte);
913                 }
914 
915                 parms = new HashMap<String, String>();
916                 if(null == headers) {
917                     headers = new HashMap<String, String>();
918                 }
919 
920                 // Create a BufferedReader for parsing the header.
921                 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
922 
923                 // Decode the header into parms and header java properties
924                 Map<String, String> pre = new HashMap<String, String>();
925                 decodeHeader(hin, pre, parms, headers);
926 
927                 method = Method.lookup(pre.get("method"));
928                 if (method == null) {
929                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
930                 }
931 
932                 uri = pre.get("uri");
933 
934                 cookies = new CookieHandler(headers);
935 
936                 // Ok, now do the serve()
937                 Response r = serve(this);
938                 if (r == null) {
939                     throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
940                 } else {
941                     cookies.unloadQueue(r);
942                     r.setRequestMethod(method);
943                     r.send(outputStream);
944                 }
945             } catch (SocketException e) {
946                 // throw it out to close socket object (finalAccept)
947                 throw e;
948             } catch (SocketTimeoutException ste) {
949             	throw ste;
950             } catch (IOException ioe) {
951                 Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
952                 r.send(outputStream);
953                 safeClose(outputStream);
954             } catch (ResponseException re) {
955                 Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
956                 r.send(outputStream);
957                 safeClose(outputStream);
958             } finally {
959                 tempFileManager.clear();
960             }
961         }
962 
963         @Override
parseBody(Map<String, String> files)964         public void parseBody(Map<String, String> files) throws IOException, ResponseException {
965             RandomAccessFile randomAccessFile = null;
966             BufferedReader in = null;
967             try {
968 
969                 randomAccessFile = getTmpBucket();
970 
971                 long size;
972                 if (headers.containsKey("content-length")) {
973                     size = Integer.parseInt(headers.get("content-length"));
974                 } else if (splitbyte < rlen) {
975                     size = rlen - splitbyte;
976                 } else {
977                     size = 0;
978                 }
979 
980                 // Now read all the body and write it to f
981                 byte[] buf = new byte[512];
982                 while (rlen >= 0 && size > 0) {
983                     rlen = inputStream.read(buf, 0, (int)Math.min(size, 512));
984                     size -= rlen;
985                     if (rlen > 0) {
986                         randomAccessFile.write(buf, 0, rlen);
987                     }
988                 }
989 
990                 // Get the raw body as a byte []
991                 ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
992                 randomAccessFile.seek(0);
993 
994                 // Create a BufferedReader for easily reading it as string.
995                 InputStream bin = new FileInputStream(randomAccessFile.getFD());
996                 in = new BufferedReader(new InputStreamReader(bin));
997 
998                 // If the method is POST, there may be parameters
999                 // in data section, too, read it:
1000                 if (Method.POST.equals(method)) {
1001                     String contentType = "";
1002                     String contentTypeHeader = headers.get("content-type");
1003 
1004                     StringTokenizer st = null;
1005                     if (contentTypeHeader != null) {
1006                         st = new StringTokenizer(contentTypeHeader, ",; ");
1007                         if (st.hasMoreTokens()) {
1008                             contentType = st.nextToken();
1009                         }
1010                     }
1011 
1012                     if ("multipart/form-data".equalsIgnoreCase(contentType)) {
1013                         // Handle multipart/form-data
1014                         if (!st.hasMoreTokens()) {
1015                             throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
1016                         }
1017 
1018                         String boundaryStartString = "boundary=";
1019                         int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
1020                         String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
1021                         if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
1022                             boundary = boundary.substring(1, boundary.length() - 1);
1023                         }
1024 
1025                         decodeMultipartData(boundary, fbuf, in, parms, files);
1026                     } else {
1027                         String postLine = "";
1028                         StringBuilder postLineBuffer = new StringBuilder();
1029                         char pbuf[] = new char[512];
1030                         int read = in.read(pbuf);
1031                         while (read >= 0 && !postLine.endsWith("\r\n")) {
1032                             postLine = String.valueOf(pbuf, 0, read);
1033                             postLineBuffer.append(postLine);
1034                             read = in.read(pbuf);
1035                         }
1036                         postLine = postLineBuffer.toString().trim();
1037                         // Handle application/x-www-form-urlencoded
1038                         if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
1039                         	decodeParms(postLine, parms);
1040                         } else if (postLine.length() != 0) {
1041                         	// Special case for raw POST data => create a special files entry "postData" with raw content data
1042                         	files.put("postData", postLine);
1043                         }
1044                     }
1045                 } else if (Method.PUT.equals(method)) {
1046                     files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
1047                 }
1048             } finally {
1049                 safeClose(randomAccessFile);
1050                 safeClose(in);
1051             }
1052         }
1053 
1054         /**
1055          * Decodes the sent headers and loads the data into Key/value pairs
1056          */
decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)1057         private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
1058             throws ResponseException {
1059             try {
1060                 // Read the request line
1061                 String inLine = in.readLine();
1062                 if (inLine == null) {
1063                     return;
1064                 }
1065 
1066                 StringTokenizer st = new StringTokenizer(inLine);
1067                 if (!st.hasMoreTokens()) {
1068                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
1069                 }
1070 
1071                 pre.put("method", st.nextToken());
1072 
1073                 if (!st.hasMoreTokens()) {
1074                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
1075                 }
1076 
1077                 String uri = st.nextToken();
1078 
1079                 // Decode parameters from the URI
1080                 int qmi = uri.indexOf('?');
1081                 if (qmi >= 0) {
1082                     decodeParms(uri.substring(qmi + 1), parms);
1083                     uri = decodePercent(uri.substring(0, qmi));
1084                 } else {
1085                     uri = decodePercent(uri);
1086                 }
1087 
1088                 // If there's another token, it's protocol version,
1089                 // followed by HTTP headers. Ignore version but parse headers.
1090                 // NOTE: this now forces header names lowercase since they are
1091                 // case insensitive and vary by client.
1092                 if (st.hasMoreTokens()) {
1093                     String line = in.readLine();
1094                     while (line != null && line.trim().length() > 0) {
1095                         int p = line.indexOf(':');
1096                         if (p >= 0)
1097                             headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
1098                         line = in.readLine();
1099                     }
1100                 }
1101 
1102                 pre.put("uri", uri);
1103             } catch (IOException ioe) {
1104                 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
1105             }
1106         }
1107 
1108         /**
1109          * Decodes the Multipart Body data and put it into Key/Value pairs.
1110          */
decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, Map<String, String> files)1111         private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms,
1112                                          Map<String, String> files) throws ResponseException {
1113             try {
1114                 int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
1115                 int boundarycount = 1;
1116                 String mpline = in.readLine();
1117                 while (mpline != null) {
1118                     if (!mpline.contains(boundary)) {
1119                         throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
1120                     }
1121                     boundarycount++;
1122                     Map<String, String> item = new HashMap<String, String>();
1123                     mpline = in.readLine();
1124                     while (mpline != null && mpline.trim().length() > 0) {
1125                         int p = mpline.indexOf(':');
1126                         if (p != -1) {
1127                             item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim());
1128                         }
1129                         mpline = in.readLine();
1130                     }
1131                     if (mpline != null) {
1132                         String contentDisposition = item.get("content-disposition");
1133                         if (contentDisposition == null) {
1134                             throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
1135                         }
1136                         StringTokenizer st = new StringTokenizer(contentDisposition, ";");
1137                         Map<String, String> disposition = new HashMap<String, String>();
1138                         while (st.hasMoreTokens()) {
1139                             String token = st.nextToken().trim();
1140                             int p = token.indexOf('=');
1141                             if (p != -1) {
1142                                 disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim());
1143                             }
1144                         }
1145                         String pname = disposition.get("name");
1146                         pname = pname.substring(1, pname.length() - 1);
1147 
1148                         String value = "";
1149                         if (item.get("content-type") == null) {
1150                             while (mpline != null && !mpline.contains(boundary)) {
1151                                 mpline = in.readLine();
1152                                 if (mpline != null) {
1153                                     int d = mpline.indexOf(boundary);
1154                                     if (d == -1) {
1155                                         value += mpline;
1156                                     } else {
1157                                         value += mpline.substring(0, d - 2);
1158                                     }
1159                                 }
1160                             }
1161                         } else {
1162                             if (boundarycount > bpositions.length) {
1163                                 throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request");
1164                             }
1165                             int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
1166                             String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
1167                             files.put(pname, path);
1168                             value = disposition.get("filename");
1169                             value = value.substring(1, value.length() - 1);
1170                             do {
1171                                 mpline = in.readLine();
1172                             } while (mpline != null && !mpline.contains(boundary));
1173                         }
1174                         parms.put(pname, value);
1175                     }
1176                 }
1177             } catch (IOException ioe) {
1178                 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
1179             }
1180         }
1181 
1182         /**
1183          * Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
1184          */
findHeaderEnd(final byte[] buf, int rlen)1185         private int findHeaderEnd(final byte[] buf, int rlen) {
1186             int splitbyte = 0;
1187             while (splitbyte + 3 < rlen) {
1188                 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
1189                     return splitbyte + 4;
1190                 }
1191                 splitbyte++;
1192             }
1193             return 0;
1194         }
1195 
1196         /**
1197          * Find the byte positions where multipart boundaries start.
1198          */
getBoundaryPositions(ByteBuffer b, byte[] boundary)1199         private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
1200             int matchcount = 0;
1201             int matchbyte = -1;
1202             List<Integer> matchbytes = new ArrayList<Integer>();
1203             for (int i = 0; i < b.limit(); i++) {
1204                 if (b.get(i) == boundary[matchcount]) {
1205                     if (matchcount == 0)
1206                         matchbyte = i;
1207                     matchcount++;
1208                     if (matchcount == boundary.length) {
1209                         matchbytes.add(matchbyte);
1210                         matchcount = 0;
1211                         matchbyte = -1;
1212                     }
1213                 } else {
1214                     i -= matchcount;
1215                     matchcount = 0;
1216                     matchbyte = -1;
1217                 }
1218             }
1219             int[] ret = new int[matchbytes.size()];
1220             for (int i = 0; i < ret.length; i++) {
1221                 ret[i] = matchbytes.get(i);
1222             }
1223             return ret;
1224         }
1225 
1226         /**
1227          * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
1228          */
saveTmpFile(ByteBuffer b, int offset, int len)1229         private String saveTmpFile(ByteBuffer b, int offset, int len) {
1230             String path = "";
1231             if (len > 0) {
1232                 FileOutputStream fileOutputStream = null;
1233                 try {
1234                     TempFile tempFile = tempFileManager.createTempFile();
1235                     ByteBuffer src = b.duplicate();
1236                     fileOutputStream = new FileOutputStream(tempFile.getName());
1237                     FileChannel dest = fileOutputStream.getChannel();
1238                     src.position(offset).limit(offset + len);
1239                     dest.write(src.slice());
1240                     path = tempFile.getName();
1241                 } catch (Exception e) { // Catch exception if any
1242                     throw new Error(e); // we won't recover, so throw an error
1243                 } finally {
1244                     safeClose(fileOutputStream);
1245                 }
1246             }
1247             return path;
1248         }
1249 
getTmpBucket()1250         private RandomAccessFile getTmpBucket() {
1251             try {
1252                 TempFile tempFile = tempFileManager.createTempFile();
1253                 return new RandomAccessFile(tempFile.getName(), "rw");
1254             } catch (Exception e) {
1255             	throw new Error(e); // we won't recover, so throw an error
1256             }
1257         }
1258 
1259         /**
1260          * It returns the offset separating multipart file headers from the file's data.
1261          */
stripMultipartHeaders(ByteBuffer b, int offset)1262         private int stripMultipartHeaders(ByteBuffer b, int offset) {
1263             int i;
1264             for (i = offset; i < b.limit(); i++) {
1265                 if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') {
1266                     break;
1267                 }
1268             }
1269             return i + 1;
1270         }
1271 
1272         /**
1273          * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
1274          * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map.
1275          */
decodeParms(String parms, Map<String, String> p)1276         private void decodeParms(String parms, Map<String, String> p) {
1277             if (parms == null) {
1278                 queryParameterString = "";
1279                 return;
1280             }
1281 
1282             queryParameterString = parms;
1283             StringTokenizer st = new StringTokenizer(parms, "&");
1284             while (st.hasMoreTokens()) {
1285                 String e = st.nextToken();
1286                 int sep = e.indexOf('=');
1287                 if (sep >= 0) {
1288                     p.put(decodePercent(e.substring(0, sep)).trim(),
1289                         decodePercent(e.substring(sep + 1)));
1290                 } else {
1291                     p.put(decodePercent(e).trim(), "");
1292                 }
1293             }
1294         }
1295 
1296         @Override
getParms()1297         public final Map<String, String> getParms() {
1298             return parms;
1299         }
1300 
getQueryParameterString()1301 		public String getQueryParameterString() {
1302             return queryParameterString;
1303         }
1304 
1305         @Override
getHeaders()1306         public final Map<String, String> getHeaders() {
1307             return headers;
1308         }
1309 
1310         @Override
getUri()1311         public final String getUri() {
1312             return uri;
1313         }
1314 
1315         @Override
getMethod()1316         public final Method getMethod() {
1317             return method;
1318         }
1319 
1320         @Override
getInputStream()1321         public final InputStream getInputStream() {
1322             return inputStream;
1323         }
1324 
1325         @Override
getCookies()1326         public CookieHandler getCookies() {
1327             return cookies;
1328         }
1329     }
1330 
1331     public static class Cookie {
1332         private String n, v, e;
1333 
Cookie(String name, String value, String expires)1334         public Cookie(String name, String value, String expires) {
1335             n = name;
1336             v = value;
1337             e = expires;
1338         }
1339 
Cookie(String name, String value)1340         public Cookie(String name, String value) {
1341             this(name, value, 30);
1342         }
1343 
Cookie(String name, String value, int numDays)1344         public Cookie(String name, String value, int numDays) {
1345             n = name;
1346             v = value;
1347             e = getHTTPTime(numDays);
1348         }
1349 
getHTTPHeader()1350         public String getHTTPHeader() {
1351             String fmt = "%s=%s; expires=%s";
1352             return String.format(fmt, n, v, e);
1353         }
1354 
getHTTPTime(int days)1355         public static String getHTTPTime(int days) {
1356             Calendar calendar = Calendar.getInstance();
1357             SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
1358             dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
1359             calendar.add(Calendar.DAY_OF_MONTH, days);
1360             return dateFormat.format(calendar.getTime());
1361         }
1362     }
1363 
1364     /**
1365      * Provides rudimentary support for cookies.
1366      * Doesn't support 'path', 'secure' nor 'httpOnly'.
1367      * Feel free to improve it and/or add unsupported features.
1368      *
1369      * @author LordFokas
1370      */
1371     public class CookieHandler implements Iterable<String> {
1372         private HashMap<String, String> cookies = new HashMap<String, String>();
1373         private ArrayList<Cookie> queue = new ArrayList<Cookie>();
1374 
CookieHandler(Map<String, String> httpHeaders)1375         public CookieHandler(Map<String, String> httpHeaders) {
1376             String raw = httpHeaders.get("cookie");
1377             if (raw != null) {
1378                 String[] tokens = raw.split(";");
1379                 for (String token : tokens) {
1380                     String[] data = token.trim().split("=");
1381                     if (data.length == 2) {
1382                         cookies.put(data[0], data[1]);
1383                     }
1384                 }
1385             }
1386         }
1387 
iterator()1388         @Override public Iterator<String> iterator() {
1389             return cookies.keySet().iterator();
1390         }
1391 
1392         /**
1393          * Read a cookie from the HTTP Headers.
1394          *
1395          * @param name The cookie's name.
1396          * @return The cookie's value if it exists, null otherwise.
1397          */
read(String name)1398         public String read(String name) {
1399             return cookies.get(name);
1400         }
1401 
1402         /**
1403          * Sets a cookie.
1404          *
1405          * @param name    The cookie's name.
1406          * @param value   The cookie's value.
1407          * @param expires How many days until the cookie expires.
1408          */
set(String name, String value, int expires)1409         public void set(String name, String value, int expires) {
1410             queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
1411         }
1412 
set(Cookie cookie)1413         public void set(Cookie cookie) {
1414             queue.add(cookie);
1415         }
1416 
1417         /**
1418          * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side.
1419          *
1420          * @param name The cookie name.
1421          */
delete(String name)1422         public void delete(String name) {
1423             set(name, "-delete-", -30);
1424         }
1425 
1426         /**
1427          * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers.
1428          *
1429          * @param response The Response object to which headers the queued cookies will be added.
1430          */
unloadQueue(Response response)1431         public void unloadQueue(Response response) {
1432             for (Cookie cookie : queue) {
1433                 response.addHeader("Set-Cookie", cookie.getHTTPHeader());
1434             }
1435         }
1436     }
1437 }
1438