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