NanoHTTPD.java revision dfea30a36949bc24a698ac39d8a714fc8f1c9e82
1package fi.iki.elonen;
2
3import java.io.*;
4import java.net.InetSocketAddress;
5import java.net.ServerSocket;
6import java.net.Socket;
7import java.nio.ByteBuffer;
8import java.nio.channels.FileChannel;
9import java.text.SimpleDateFormat;
10import java.util.*;
11
12/**
13 * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
14 * <p/>
15 * <p/>
16 * NanoHTTPD version 1.25, Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) and Copyright &copy; 2010
17 * Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
18 * <p/>
19 * Uplifted to Java5 by Micah Hainline and Paul Hawke (paul.hawke@gmail.com).
20 * <p/>
21 * <p/>
22 * <b>Features + limitations: </b>
23 * <ul>
24 * <p/>
25 * <li>Only one Java file</li>
26 * <li>Java 5 compatible</li>
27 * <li>Released as open source, Modified BSD licence</li>
28 * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
29 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
30 * <li>Supports both dynamic content and file serving</li>
31 * <li>Supports file upload (since version 1.2, 2010)</li>
32 * <li>Supports partial content (streaming)</li>
33 * <li>Supports ETags</li>
34 * <li>Never caches anything</li>
35 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
36 * <li>Default code serves files and shows all HTTP parameters and headers</li>
37 * <li>File server supports directory listing, index.html and index.htm</li>
38 * <li>File server supports partial content (streaming)</li>
39 * <li>File server supports ETags</li>
40 * <li>File server does the 301 redirection trick for directories without '/'</li>
41 * <li>File server supports simple skipping for files (continue download)</li>
42 * <li>File server serves also very long files without memory overhead</li>
43 * <li>Contains a built-in list of most common mime types</li>
44 * <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
45 * <p/>
46 * </ul>
47 * <p/>
48 * <p/>
49 * <b>How to use: </b>
50 * <ul>
51 * <p/>
52 * <li>Subclass and implement serve() and embed to your own program</li>
53 * <p/>
54 * </ul>
55 * <p/>
56 * See the end of the source file for distribution license (Modified BSD licence)
57 */
58public abstract class NanoHTTPD {
59    /*
60     * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing.
61     */
62    public static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
63    /**
64     * Common mime types for dynamic content
65     */
66    public static final String MIME_PLAINTEXT = "text/plain";
67    public static final String MIME_HTML = "text/html";
68    public static final String MIME_DEFAULT_BINARY = "application/octet-stream";
69    private final String hostname;
70    private final int myPort;
71    private ServerSocket myServerSocket;
72    private Thread myThread;
73    private TempFileManagerFactory tempFileManagerFactory;
74    private AsyncRunner asyncRunner;
75
76    /**
77     * Constructs an HTTP server on given port.
78     */
79    public NanoHTTPD(int port) {
80        this(null, port);
81    }
82
83    public NanoHTTPD(String hostname, int port) {
84        this.hostname = hostname;
85        this.myPort = port;
86        this.tempFileManagerFactory = new DefaultTempFileManagerFactory();
87        this.asyncRunner = new DefaultAsyncRunner();
88    }
89
90    /**
91     * Starts the server
92     * <p/>
93     * Throws an IOException if the socket is already in use
94     */
95    public void start() throws IOException {
96        myServerSocket = new ServerSocket();
97        myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
98
99        myThread = new Thread(new Runnable() {
100            @Override
101            public void run() {
102                do {
103                    try {
104                        final Socket finalAccept = myServerSocket.accept();
105                        InputStream inputStream = finalAccept.getInputStream();
106                        OutputStream outputStream = finalAccept.getOutputStream();
107                        TempFileManager tempFileManager = tempFileManagerFactory.create();
108                        final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);
109                        asyncRunner.exec(new Runnable() {
110                            @Override
111                            public void run() {
112                                session.run();
113                                if (finalAccept != null) {
114                                    try {
115                                        finalAccept.close();
116                                    } catch (IOException ignored) {
117                                    }
118                                }
119                            }
120                        });
121                    } catch (IOException e) {
122                    }
123                } while (true);
124            }
125        });
126        myThread.setDaemon(true);
127        myThread.start();
128    }
129
130    /**
131     * Stops the server.
132     */
133    public void stop() {
134        try {
135            myServerSocket.close();
136            myThread.join();
137        } catch (IOException ioe) {
138            ioe.printStackTrace();
139        } catch (InterruptedException e) {
140            e.printStackTrace();
141        }
142    }
143
144    public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
145        this.tempFileManagerFactory = tempFileManagerFactory;
146    }
147
148    public void setAsyncRunner(AsyncRunner asyncRunner) {
149        this.asyncRunner = asyncRunner;
150    }
151
152    /**
153     * Override this to customize the server.
154     * <p/>
155     * <p/>
156     * (By default, this delegates to serveFile() and allows directory listing.)
157     *
158     * @param uri    Percent-decoded URI without parameters, for example "/index.cgi"
159     * @param method "GET", "POST" etc.
160     * @param parms  Parsed, percent decoded parameters from URI and, in case of POST, data.
161     * @param header Header entries, percent decoded
162     * @return HTTP response, see class Response for details
163     */
164    public abstract Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms,
165                                   Map<String, String> files);
166
167    /**
168     * Decodes the percent encoding scheme. <br/>
169     * For example: "an+example%20string" -> "an example string"
170     */
171    protected String decodePercent(String str) throws InterruptedException {
172        try {
173            StringBuilder sb = new StringBuilder();
174            for (int i = 0; i < str.length(); i++) {
175                char c = str.charAt(i);
176                switch (c) {
177                    case '+':
178                        sb.append(' ');
179                        break;
180                    case '%':
181                        sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16));
182                        i += 2;
183                        break;
184                    default:
185                        sb.append(c);
186                        break;
187                }
188            }
189            return sb.toString();
190        } catch (Exception e) {
191            throw new InterruptedException();
192        }
193    }
194
195    protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
196        return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
197    }
198
199    protected Map<String, List<String>> decodeParameters(String queryString) {
200        Map<String, List<String>> parms = new HashMap<String, List<String>>();
201        if (queryString != null) {
202            StringTokenizer st = new StringTokenizer(queryString, "&");
203            while (st.hasMoreTokens()) {
204                String e = st.nextToken();
205                int sep = e.indexOf('=');
206                try {
207                    String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
208                    if (!parms.containsKey(propertyName)) {
209                        parms.put(propertyName, new ArrayList<String>());
210                    }
211                    String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
212                    if (propertyValue != null) {
213                        parms.get(propertyName).add(propertyValue);
214                    }
215                } catch (InterruptedException e1) {
216                    e1.printStackTrace();
217                }
218            }
219        }
220        return parms;
221    }
222
223    public enum Method {
224        GET, PUT, POST, DELETE;
225
226        static Method lookup(String method) {
227            for (Method m : Method.values()) {
228                if (m.toString().equalsIgnoreCase(method)) {
229                    return m;
230                }
231            }
232            return null;
233        }
234    }
235
236    public interface AsyncRunner {
237        void exec(Runnable code);
238    }
239
240    public interface TempFileManagerFactory {
241        TempFileManager create();
242    }
243
244    public interface TempFileManager {
245        TempFile createTempFile() throws Exception;
246
247        void clear();
248    }
249
250    public interface TempFile {
251        OutputStream open() throws Exception;
252
253        void delete() throws Exception;
254
255        String getName();
256    }
257
258    /**
259     * HTTP response. Return one of these from serve().
260     */
261    public static class Response {
262        /**
263         * HTTP status code after processing, e.g. "200 OK", HTTP_OK
264         */
265        public Status status;
266        /**
267         * MIME type of content, e.g. "text/html"
268         */
269        public String mimeType;
270        /**
271         * Data of the response, may be null.
272         */
273        public InputStream data;
274        /**
275         * Headers for the HTTP response. Use addHeader() to add lines.
276         */
277        public Map<String, String> header = new HashMap<String, String>();
278
279        /**
280         * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
281         */
282        public Response(String msg) {
283            this(Status.OK, MIME_HTML, msg);
284        }
285
286        /**
287         * Basic constructor.
288         */
289        public Response(Status status, String mimeType, InputStream data) {
290            this.status = status;
291            this.mimeType = mimeType;
292            this.data = data;
293        }
294
295        /**
296         * Convenience method that makes an InputStream out of given text.
297         */
298        public Response(Status status, String mimeType, String txt) {
299            this.status = status;
300            this.mimeType = mimeType;
301            try {
302                this.data = new ByteArrayInputStream(txt.getBytes("UTF-8"));
303            } catch (java.io.UnsupportedEncodingException uee) {
304                uee.printStackTrace();
305            }
306        }
307
308        public static void error(OutputStream outputStream, Status error, String message) {
309            new Response(error, MIME_PLAINTEXT, message).send(outputStream);
310        }
311
312        /**
313         * Adds given line to the header.
314         */
315        public void addHeader(String name, String value) {
316            header.put(name, value);
317        }
318
319        /**
320         * Sends given response to the socket.
321         */
322        private void send(OutputStream outputStream) {
323            String mime = mimeType;
324            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
325            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
326
327            try {
328                if (status == null) {
329                    throw new Error("sendResponse(): Status can't be null.");
330                }
331                PrintWriter pw = new PrintWriter(outputStream);
332                pw.print("HTTP/1.0 " + status.getDescription() + " \r\n");
333
334                if (mime != null) {
335                    pw.print("Content-Type: " + mime + "\r\n");
336                }
337
338                if (header == null || header.get("Date") == null) {
339                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
340                }
341
342                if (header != null) {
343                    for (String key : header.keySet()) {
344                        String value = header.get(key);
345                        pw.print(key + ": " + value + "\r\n");
346                    }
347                }
348
349                pw.print("\r\n");
350                pw.flush();
351
352                if (data != null) {
353                    int pending = data.available(); // This is to support partial sends, see serveFile()
354                    int BUFFER_SIZE = 16 * 1024;
355                    byte[] buff = new byte[BUFFER_SIZE];
356                    while (pending > 0) {
357                        int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
358                        if (read <= 0) {
359                            break;
360                        }
361                        outputStream.write(buff, 0, read);
362                        pending -= read;
363                    }
364                }
365                outputStream.flush();
366                outputStream.close();
367                if (data != null)
368                    data.close();
369            } catch (IOException ioe) {
370                // Couldn't write? No can do.
371            }
372        }
373
374        /**
375         * Some HTTP response status codes
376         */
377        public enum Status {
378            OK(200, "OK"), CREATED(201, "Created"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301,
379                    "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401,
380                    "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), RANGE_NOT_SATISFIABLE(416,
381                    "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error");
382            private int requestStatus;
383            private String descr;
384
385            Status(int requestStatus, String descr) {
386                this.requestStatus = requestStatus;
387                this.descr = descr;
388            }
389
390            public int getRequestStatus() {
391                return this.requestStatus;
392            }
393
394            public String getDescription() {
395                return "" + this.requestStatus + " " + descr;
396            }
397        }
398    }
399
400    public static class DefaultTempFile implements TempFile {
401        private File file;
402        private OutputStream fstream;
403
404        public DefaultTempFile(String tempdir) throws IOException {
405            file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
406            fstream = new FileOutputStream(file);
407        }
408
409        @Override
410        public OutputStream open() throws Exception {
411            return fstream;
412        }
413
414        @Override
415        public void delete() throws Exception {
416            file.delete();
417        }
418
419        @Override
420        public String getName() {
421            return file.getAbsolutePath();
422        }
423    }
424
425    /**
426     * Handles one session, i.e. parses the HTTP request and returns the response.
427     */
428    protected class HTTPSession implements Runnable {
429        public static final int BUFSIZE = 8192;
430        private final TempFileManager tempFileManager;
431        private InputStream inputStream;
432        private OutputStream outputStream;
433
434        public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
435            this.tempFileManager = tempFileManager;
436            this.inputStream = inputStream;
437            this.outputStream = outputStream;
438        }
439
440        @Override
441        public void run() {
442            try {
443                if (inputStream == null) {
444                    return;
445                }
446
447                // Read the first 8192 bytes.
448                // The full header should fit in here.
449                // Apache's default header limit is 8KB.
450                // Do NOT assume that a single read will get the entire header at once!
451                byte[] buf = new byte[BUFSIZE];
452                int splitbyte = 0;
453                int rlen = 0;
454                {
455                    int read = inputStream.read(buf, 0, BUFSIZE);
456                    while (read > 0) {
457                        rlen += read;
458                        splitbyte = findHeaderEnd(buf, rlen);
459                        if (splitbyte > 0)
460                            break;
461                        read = inputStream.read(buf, rlen, BUFSIZE - rlen);
462                    }
463                }
464
465                // Create a BufferedReader for parsing the header.
466                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
467                Map<String, String> pre = new HashMap<String, String>();
468                Map<String, String> parms = new HashMap<String, String>();
469                Map<String, String> header = new HashMap<String, String>();
470                Map<String, String> files = new HashMap<String, String>();
471
472                // Decode the header into parms and header java properties
473                decodeHeader(hin, pre, parms, header);
474                Method method = Method.lookup(pre.get("method"));
475                if (method == null) {
476                    Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
477                    throw new InterruptedException();
478                }
479                String uri = pre.get("uri");
480                long size = extractContentLength(header);
481
482                // Write the part of body already read to ByteArrayOutputStream f
483                RandomAccessFile f = getTmpBucket();
484                if (splitbyte < rlen) {
485                    f.write(buf, splitbyte, rlen - splitbyte);
486                }
487
488                // While Firefox sends on the first read all the data fitting
489                // our buffer, Chrome and Opera send only the headers even if
490                // there is data for the body. We do some magic here to find
491                // out whether we have already consumed part of body, if we
492                // have reached the end of the data to be sent or we should
493                // expect the first byte of the body at the next read.
494                if (splitbyte < rlen) {
495                    size -= rlen - splitbyte + 1;
496                } else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) {
497                    size = 0;
498                }
499
500                // Now read all the body and write it to f
501                buf = new byte[512];
502                while (rlen >= 0 && size > 0) {
503                    rlen = inputStream.read(buf, 0, 512);
504                    size -= rlen;
505                    if (rlen > 0) {
506                        f.write(buf, 0, rlen);
507                    }
508                }
509
510                // Get the raw body as a byte []
511                ByteBuffer fbuf = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, f.length());
512                f.seek(0);
513
514                // Create a BufferedReader for easily reading it as string.
515                InputStream bin = new FileInputStream(f.getFD());
516                BufferedReader in = new BufferedReader(new InputStreamReader(bin));
517
518                // If the method is POST, there may be parameters
519                // in data section, too, read it:
520                if (Method.POST.equals(method)) {
521                    String contentType = "";
522                    String contentTypeHeader = header.get("content-type");
523
524                    StringTokenizer st = null;
525                    if (contentTypeHeader != null) {
526                        st = new StringTokenizer(contentTypeHeader, ",; ");
527                        if (st.hasMoreTokens()) {
528                            contentType = st.nextToken();
529                        }
530                    }
531
532                    if ("multipart/form-data".equalsIgnoreCase(contentType)) {
533                        // Handle multipart/form-data
534                        if (!st.hasMoreTokens()) {
535                            Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
536                            throw new InterruptedException();
537                        }
538
539                        String boundaryStartString = "boundary=";
540                        int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
541                        String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
542                        if (boundary.startsWith("\"") && boundary.startsWith("\"")) {
543                            boundary = boundary.substring(1, boundary.length() - 1);
544                        }
545
546                        decodeMultipartData(boundary, fbuf, in, parms, files);
547                    } else {
548                        // Handle application/x-www-form-urlencoded
549                        String postLine = "";
550                        char pbuf[] = new char[512];
551                        int read = in.read(pbuf);
552                        while (read >= 0 && !postLine.endsWith("\r\n")) {
553                            postLine += String.valueOf(pbuf, 0, read);
554                            read = in.read(pbuf);
555                        }
556                        postLine = postLine.trim();
557                        decodeParms(postLine, parms);
558                    }
559                }
560
561                if (Method.PUT.equals(method))
562                    files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
563
564                // Ok, now do the serve()
565                Response r = serve(uri, method, header, parms, files);
566                if (r == null) {
567                    Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
568                    throw new InterruptedException();
569                } else {
570                    r.send(outputStream);
571                }
572
573                in.close();
574                inputStream.close();
575            } catch (IOException ioe) {
576                try {
577                    Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
578                    throw new InterruptedException();
579                } catch (Throwable ignored) {
580                }
581            } catch (InterruptedException ie) {
582                // Thrown by sendError, ignore and exit the thread.
583            } finally {
584                tempFileManager.clear();
585            }
586        }
587
588        private long extractContentLength(Map<String, String> header) {
589            long size = 0x7FFFFFFFFFFFFFFFl;
590            String contentLength = header.get("content-length");
591            if (contentLength != null) {
592                try {
593                    size = Integer.parseInt(contentLength);
594                } catch (NumberFormatException ex) {
595                    ex.printStackTrace();
596                }
597            }
598            return size;
599        }
600
601        /**
602         * Decodes the sent headers and loads the data into Key/value pairs
603         */
604        private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> header)
605                throws InterruptedException {
606            try {
607                // Read the request line
608                String inLine = in.readLine();
609                if (inLine == null) {
610                    return;
611                }
612
613                StringTokenizer st = new StringTokenizer(inLine);
614                if (!st.hasMoreTokens()) {
615                    Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
616                    throw new InterruptedException();
617                }
618
619                pre.put("method", st.nextToken());
620
621                if (!st.hasMoreTokens()) {
622                    Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
623                    throw new InterruptedException();
624                }
625
626                String uri = st.nextToken();
627
628                // Decode parameters from the URI
629                int qmi = uri.indexOf('?');
630                if (qmi >= 0) {
631                    decodeParms(uri.substring(qmi + 1), parms);
632                    uri = decodePercent(uri.substring(0, qmi));
633                } else {
634                    uri = decodePercent(uri);
635                }
636
637                // If there's another token, it's protocol version,
638                // followed by HTTP headers. Ignore version but parse headers.
639                // NOTE: this now forces header names lowercase since they are
640                // case insensitive and vary by client.
641                if (st.hasMoreTokens()) {
642                    String line = in.readLine();
643                    while (line != null && line.trim().length() > 0) {
644                        int p = line.indexOf(':');
645                        if (p >= 0)
646                            header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
647                        line = in.readLine();
648                    }
649                }
650
651                pre.put("uri", uri);
652            } catch (IOException ioe) {
653                Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
654                throw new InterruptedException();
655            }
656        }
657
658        /**
659         * Decodes the Multipart Body data and put it into Key/Value pairs.
660         */
661        private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms,
662                                         Map<String, String> files) throws InterruptedException {
663            try {
664                int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
665                int boundarycount = 1;
666                String mpline = in.readLine();
667                while (mpline != null) {
668                    if (!mpline.contains(boundary)) {
669                        Response.error(outputStream, 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");
670                        throw new InterruptedException();
671                    }
672                    boundarycount++;
673                    Map<String, String> item = new HashMap<String, String>();
674                    mpline = in.readLine();
675                    while (mpline != null && mpline.trim().length() > 0) {
676                        int p = mpline.indexOf(':');
677                        if (p != -1) {
678                            item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim());
679                        }
680                        mpline = in.readLine();
681                    }
682                    if (mpline != null) {
683                        String contentDisposition = item.get("content-disposition");
684                        if (contentDisposition == null) {
685                            Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
686                            throw new InterruptedException();
687                        }
688                        StringTokenizer st = new StringTokenizer(contentDisposition, "; ");
689                        Map<String, String> disposition = new HashMap<String, String>();
690                        while (st.hasMoreTokens()) {
691                            String token = st.nextToken();
692                            int p = token.indexOf('=');
693                            if (p != -1) {
694                                disposition.put(token.substring(0, p).trim().toLowerCase(), token.substring(p + 1).trim());
695                            }
696                        }
697                        String pname = disposition.get("name");
698                        pname = pname.substring(1, pname.length() - 1);
699
700                        String value = "";
701                        if (item.get("content-type") == null) {
702                            while (mpline != null && !mpline.contains(boundary)) {
703                                mpline = in.readLine();
704                                if (mpline != null) {
705                                    int d = mpline.indexOf(boundary);
706                                    if (d == -1) {
707                                        value += mpline;
708                                    } else {
709                                        value += mpline.substring(0, d - 2);
710                                    }
711                                }
712                            }
713                        } else {
714                            if (boundarycount > bpositions.length) {
715                                Response.error(outputStream, Response.Status.INTERNAL_ERROR, "Error processing request");
716                                throw new InterruptedException();
717                            }
718                            int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
719                            String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
720                            files.put(pname, path);
721                            value = disposition.get("filename");
722                            value = value.substring(1, value.length() - 1);
723                            do {
724                                mpline = in.readLine();
725                            } while (mpline != null && !mpline.contains(boundary));
726                        }
727                        parms.put(pname, value);
728                    }
729                }
730            } catch (IOException ioe) {
731                Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
732                throw new InterruptedException();
733            }
734        }
735
736        /**
737         * Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
738         */
739        private int findHeaderEnd(final byte[] buf, int rlen) {
740            int splitbyte = 0;
741            while (splitbyte + 3 < rlen) {
742                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
743                    return splitbyte + 4;
744                }
745                splitbyte++;
746            }
747            return 0;
748        }
749
750        /**
751         * Find the byte positions where multipart boundaries start.
752         */
753        public int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
754            int matchcount = 0;
755            int matchbyte = -1;
756            List<Integer> matchbytes = new ArrayList<Integer>();
757            for (int i=0; i<b.limit(); i++) {
758                if (b.get(i) == boundary[matchcount]) {
759                    if (matchcount == 0)
760                        matchbyte = i;
761                    matchcount++;
762                    if (matchcount == boundary.length) {
763                        matchbytes.add(matchbyte);
764                        matchcount = 0;
765                        matchbyte = -1;
766                    }
767                } else {
768                    i -= matchcount;
769                    matchcount = 0;
770                    matchbyte = -1;
771                }
772            }
773            int[] ret = new int[matchbytes.size()];
774            for (int i = 0; i < ret.length; i++) {
775                ret[i] = matchbytes.get(i);
776            }
777            return ret;
778        }
779
780        /**
781         * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
782         */
783        private String saveTmpFile(ByteBuffer  b, int offset, int len) {
784            String path = "";
785            if (len > 0) {
786                try {
787                    TempFile tempFile = tempFileManager.createTempFile();
788                    ByteBuffer src = b.duplicate();
789                    FileChannel dest = new FileOutputStream(tempFile.getName()).getChannel();
790                    src.position(offset).limit(offset + len);
791                    dest.write(src.slice());
792                    path = tempFile.getName();
793                } catch (Exception e) { // Catch exception if any
794                    System.err.println("Error: " + e.getMessage());
795                }
796            }
797            return path;
798        }
799
800        private RandomAccessFile getTmpBucket() throws IOException {
801            try {
802                TempFile tempFile = tempFileManager.createTempFile();
803                return new RandomAccessFile(tempFile.getName(), "rw");
804            } catch (Exception e) {
805                System.err.println("Error: " + e.getMessage());
806            }
807            return null;
808        }
809
810        /**
811         * It returns the offset separating multipart file headers from the file's data.
812         */
813        private int stripMultipartHeaders(ByteBuffer b, int offset) {
814            int i;
815            for (i=offset; i<b.limit(); i++) {
816                if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') {
817                    break;
818                }
819            }
820            return i + 1;
821        }
822
823        /**
824         * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
825         * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map.
826         */
827        private void decodeParms(String parms, Map<String, String> p) throws InterruptedException {
828            if (parms == null) {
829                p.put(QUERY_STRING_PARAMETER, "");
830                return;
831            }
832
833            p.put(QUERY_STRING_PARAMETER, parms);
834            StringTokenizer st = new StringTokenizer(parms, "&");
835            try {
836                while (st.hasMoreTokens()) {
837                    String e = st.nextToken();
838                    int sep = e.indexOf('=');
839                    if (sep >= 0) {
840                        p.put(decodePercent(e.substring(0, sep)).trim(),
841                                decodePercent(e.substring(sep + 1)));
842                    } else {
843                        p.put(decodePercent(e).trim(), "");
844                    }
845                }
846            } catch (InterruptedException e) {
847                Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Bad percent-encoding.");
848            }
849        }
850    }
851
852    private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
853        @Override
854        public TempFileManager create() {
855            return new DefaultTempFileManager();
856        }
857    }
858
859    public static class DefaultTempFileManager implements TempFileManager {
860        private final String tmpdir;
861        private final List<TempFile> tempFiles;
862
863        public DefaultTempFileManager() {
864            tmpdir = System.getProperty("java.io.tmpdir");
865            tempFiles = new ArrayList<TempFile>();
866        }
867
868        @Override
869        public TempFile createTempFile() throws Exception {
870            DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
871            tempFiles.add(tempFile);
872            return tempFile;
873        }
874
875        @Override
876        public void clear() {
877            for (TempFile file : tempFiles) {
878                try {
879                    file.delete();
880                } catch (Exception ignored) {
881                }
882            }
883            tempFiles.clear();
884        }
885    }
886
887    private class DefaultAsyncRunner implements AsyncRunner {
888        @Override
889        public void exec(Runnable code) {
890            Thread t = new Thread(code);
891            t.setDaemon(true);
892            t.start();
893        }
894    }
895}
896