Home | History | Annotate | Download | only in elonen
      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      */
    107     public NanoHTTPD(int port) {
    108         this(null, port);
    109     }
    110 
    111     /**
    112      * Constructs an HTTP server on given hostname and port.
    113      */
    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 
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    231     public synchronized void unRegisterConnection(Socket socket) {
    232         openConnections.remove(socket);
    233     }
    234 
    235     /**
    236      * Forcibly closes all connections that are open.
    237      */
    238     public synchronized void closeAllConnections() {
    239         for (Socket socket : openConnections) {
    240             safeClose(socket);
    241         }
    242     }
    243 
    244     public final int getListeningPort() {
    245         return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
    246     }
    247 
    248     public final boolean wasStarted() {
    249         return myServerSocket != null && myThread != null;
    250     }
    251 
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 {
    406         void exec(Runnable code);
    407     }
    408 
    409     /**
    410      * Factory to create temp file managers.
    411      */
    412     public interface TempFileManagerFactory {
    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 {
    425         TempFile createTempFile() throws Exception;
    426 
    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 {
    437         OutputStream open() throws Exception;
    438 
    439         void delete() throws Exception;
    440 
    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
    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 
    477         public DefaultTempFileManager() {
    478             tmpdir = System.getProperty("java.io.tmpdir");
    479             tempFiles = new ArrayList<TempFile>();
    480         }
    481 
    482         @Override
    483         public TempFile createTempFile() throws Exception {
    484             DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
    485             tempFiles.add(tempFile);
    486             return tempFile;
    487         }
    488 
    489         @Override
    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 
    511         public DefaultTempFile(String tempdir) throws IOException {
    512             file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
    513             fstream = new FileOutputStream(file);
    514         }
    515 
    516         @Override
    517         public OutputStream open() throws Exception {
    518             return fstream;
    519         }
    520 
    521         @Override
    522         public void delete() throws Exception {
    523             safeClose(fstream);
    524             file.delete();
    525         }
    526 
    527         @Override
    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          */
    565         public Response(String msg) {
    566             this(Status.OK, MIME_HTML, msg);
    567         }
    568 
    569         /**
    570          * Basic constructor.
    571          */
    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          */
    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          */
    594         public void addHeader(String name, String value) {
    595             header.put(name, value);
    596         }
    597 
    598         public String getHeader(String name) {
    599             return header.get(name);
    600         }
    601 
    602         /**
    603          * Sends given response to the socket.
    604          */
    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 
    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 
    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 
    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 
    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 
    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 
    701         public IStatus getStatus() {
    702             return status;
    703         }
    704 
    705         public void setStatus(Status status) {
    706             this.status = status;
    707         }
    708 
    709         public String getMimeType() {
    710             return mimeType;
    711         }
    712 
    713         public void setMimeType(String mimeType) {
    714             this.mimeType = mimeType;
    715         }
    716 
    717         public InputStream getData() {
    718             return data;
    719         }
    720 
    721         public void setData(InputStream data) {
    722             this.data = data;
    723         }
    724 
    725         public Method getRequestMethod() {
    726             return requestMethod;
    727         }
    728 
    729         public void setRequestMethod(Method requestMethod) {
    730             this.requestMethod = requestMethod;
    731         }
    732 
    733         public void setChunkedTransfer(boolean chunkedTransfer) {
    734             this.chunkedTransfer = chunkedTransfer;
    735         }
    736 
    737         public interface IStatus {
    738             int getRequestStatus();
    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 
    770             Status(int requestStatus, String description) {
    771                 this.requestStatus = requestStatus;
    772                 this.description = description;
    773             }
    774 
    775             @Override
    776             public int getRequestStatus() {
    777                 return this.requestStatus;
    778             }
    779 
    780             @Override
    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 
    791         public ResponseException(Response.Status status, String message) {
    792             super(message);
    793             this.status = status;
    794         }
    795 
    796         public ResponseException(Response.Status status, String message, Exception e) {
    797             super(message, e);
    798             this.status = status;
    799         }
    800 
    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
    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 {
    820         void execute() throws IOException;
    821 
    822         Map<String, String> getParms();
    823 
    824         Map<String, String> getHeaders();
    825 
    826         /**
    827          * @return the path part of the URL.
    828          */
    829         String getUri();
    830 
    831         String getQueryParameterString();
    832 
    833         Method getMethod();
    834 
    835         InputStream getInputStream();
    836 
    837         CookieHandler getCookies();
    838 
    839         /**
    840          * Adds the files in the request body to the files map.
    841          * @arg files - map to modify
    842          */
    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 
    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 
    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
    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
    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          */
   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          */
   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          */
   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          */
   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          */
   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 
   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          */
   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          */
   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
   1297         public final Map<String, String> getParms() {
   1298             return parms;
   1299         }
   1300 
   1301 		public String getQueryParameterString() {
   1302             return queryParameterString;
   1303         }
   1304 
   1305         @Override
   1306         public final Map<String, String> getHeaders() {
   1307             return headers;
   1308         }
   1309 
   1310         @Override
   1311         public final String getUri() {
   1312             return uri;
   1313         }
   1314 
   1315         @Override
   1316         public final Method getMethod() {
   1317             return method;
   1318         }
   1319 
   1320         @Override
   1321         public final InputStream getInputStream() {
   1322             return inputStream;
   1323         }
   1324 
   1325         @Override
   1326         public CookieHandler getCookies() {
   1327             return cookies;
   1328         }
   1329     }
   1330 
   1331     public static class Cookie {
   1332         private String n, v, e;
   1333 
   1334         public Cookie(String name, String value, String expires) {
   1335             n = name;
   1336             v = value;
   1337             e = expires;
   1338         }
   1339 
   1340         public Cookie(String name, String value) {
   1341             this(name, value, 30);
   1342         }
   1343 
   1344         public Cookie(String name, String value, int numDays) {
   1345             n = name;
   1346             v = value;
   1347             e = getHTTPTime(numDays);
   1348         }
   1349 
   1350         public String getHTTPHeader() {
   1351             String fmt = "%s=%s; expires=%s";
   1352             return String.format(fmt, n, v, e);
   1353         }
   1354 
   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 
   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 
   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          */
   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          */
   1409         public void set(String name, String value, int expires) {
   1410             queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
   1411         }
   1412 
   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          */
   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          */
   1431         public void unloadQueue(Response response) {
   1432             for (Cookie cookie : queue) {
   1433                 response.addHeader("Set-Cookie", cookie.getHTTPHeader());
   1434             }
   1435         }
   1436     }
   1437 }
   1438