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