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