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