NanoHTTPD.java revision f713632b4a7ad3fa9c7581dff3df6a5abf7de395
1package fi.iki.elonen;
2
3/*
4 * #%L
5 * NanoHttpd-Core
6 * %%
7 * Copyright (C) 2012 - 2015 nanohttpd
8 * %%
9 * Redistribution and use in source and binary forms, with or without modification,
10 * are permitted provided that the following conditions are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright notice, this
13 *    list of conditions and the following disclaimer.
14 *
15 * 2. Redistributions in binary form must reproduce the above copyright notice,
16 *    this list of conditions and the following disclaimer in the documentation
17 *    and/or other materials provided with the distribution.
18 *
19 * 3. Neither the name of the nanohttpd nor the names of its contributors
20 *    may be used to endorse or promote products derived from this software without
21 *    specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32 * OF THE POSSIBILITY OF SUCH DAMAGE.
33 * #L%
34 */
35
36import java.io.*;
37import java.net.InetAddress;
38import java.net.InetSocketAddress;
39import java.net.ServerSocket;
40import java.net.Socket;
41import java.net.SocketException;
42import java.net.SocketTimeoutException;
43import java.net.URLDecoder;
44import java.nio.ByteBuffer;
45import java.nio.channels.FileChannel;
46import java.security.KeyStore;
47import java.text.SimpleDateFormat;
48import java.util.ArrayList;
49import java.util.Calendar;
50import java.util.Collections;
51import java.util.Date;
52import java.util.HashMap;
53import java.util.Iterator;
54import java.util.List;
55import java.util.Locale;
56import java.util.Map;
57import java.util.StringTokenizer;
58import java.util.TimeZone;
59import java.util.logging.Level;
60import java.util.logging.Logger;
61import java.util.regex.Matcher;
62import java.util.regex.Pattern;
63import java.util.zip.GZIPOutputStream;
64
65import javax.net.ssl.KeyManager;
66import javax.net.ssl.KeyManagerFactory;
67import javax.net.ssl.SSLContext;
68import javax.net.ssl.SSLServerSocket;
69import javax.net.ssl.SSLServerSocketFactory;
70import javax.net.ssl.TrustManagerFactory;
71
72import fi.iki.elonen.NanoHTTPD.Response.IStatus;
73import fi.iki.elonen.NanoHTTPD.Response.Status;
74
75/**
76 * A simple, tiny, nicely embeddable HTTP server in Java
77 * <p/>
78 * <p/>
79 * NanoHTTPD
80 * <p>
81 * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen,
82 * 2010 by Konstantinos Togias
83 * </p>
84 * <p/>
85 * <p/>
86 * <b>Features + limitations: </b>
87 * <ul>
88 * <p/>
89 * <li>Only one Java file</li>
90 * <li>Java 5 compatible</li>
91 * <li>Released as open source, Modified BSD licence</li>
92 * <li>No fixed config files, logging, authorization etc. (Implement yourself if
93 * you need them.)</li>
94 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT
95 * support in 1.25)</li>
96 * <li>Supports both dynamic content and file serving</li>
97 * <li>Supports file upload (since version 1.2, 2010)</li>
98 * <li>Supports partial content (streaming)</li>
99 * <li>Supports ETags</li>
100 * <li>Never caches anything</li>
101 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
102 * <li>Default code serves files and shows all HTTP parameters and headers</li>
103 * <li>File server supports directory listing, index.html and index.htm</li>
104 * <li>File server supports partial content (streaming)</li>
105 * <li>File server supports ETags</li>
106 * <li>File server does the 301 redirection trick for directories without '/'</li>
107 * <li>File server supports simple skipping for files (continue download)</li>
108 * <li>File server serves also very long files without memory overhead</li>
109 * <li>Contains a built-in list of most common MIME types</li>
110 * <li>All header names are converted to lower case so they don't vary between
111 * browsers/clients</li>
112 * <p/>
113 * </ul>
114 * <p/>
115 * <p/>
116 * <b>How to use: </b>
117 * <ul>
118 * <p/>
119 * <li>Subclass and implement serve() and embed to your own program</li>
120 * <p/>
121 * </ul>
122 * <p/>
123 * See the separate "LICENSE.md" file for the distribution license (Modified BSD
124 * licence)
125 */
126public abstract class NanoHTTPD {
127
128    /**
129     * Pluggable strategy for asynchronously executing requests.
130     */
131    public interface AsyncRunner {
132
133        void closeAll();
134
135        void closed(ClientHandler clientHandler);
136
137        void exec(ClientHandler code);
138    }
139
140    /**
141     * The runnable that will be used for every new client connection.
142     */
143    public class ClientHandler implements Runnable {
144
145        private final InputStream inputStream;
146
147        private final Socket acceptSocket;
148
149        private ClientHandler(InputStream inputStream, Socket acceptSocket) {
150            this.inputStream = inputStream;
151            this.acceptSocket = acceptSocket;
152        }
153
154        public void close() {
155            safeClose(this.inputStream);
156            safeClose(this.acceptSocket);
157        }
158
159        @Override
160        public void run() {
161            OutputStream outputStream = null;
162            try {
163                outputStream = this.acceptSocket.getOutputStream();
164                TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
165                HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
166                while (!this.acceptSocket.isClosed()) {
167                    session.execute();
168                }
169            } catch (Exception e) {
170                // When the socket is closed by the client,
171                // we throw our own SocketException
172                // to break the "keep alive" loop above. If
173                // the exception was anything other
174                // than the expected SocketException OR a
175                // SocketTimeoutException, print the
176                // stacktrace
177                if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
178                    NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
179                }
180            } finally {
181                safeClose(outputStream);
182                safeClose(this.inputStream);
183                safeClose(this.acceptSocket);
184                NanoHTTPD.this.asyncRunner.closed(this);
185            }
186        }
187    }
188
189    public static class Cookie {
190
191        public static String getHTTPTime(int days) {
192            Calendar calendar = Calendar.getInstance();
193            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
194            dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
195            calendar.add(Calendar.DAY_OF_MONTH, days);
196            return dateFormat.format(calendar.getTime());
197        }
198
199        private final String n, v, e;
200
201        public Cookie(String name, String value) {
202            this(name, value, 30);
203        }
204
205        public Cookie(String name, String value, int numDays) {
206            this.n = name;
207            this.v = value;
208            this.e = getHTTPTime(numDays);
209        }
210
211        public Cookie(String name, String value, String expires) {
212            this.n = name;
213            this.v = value;
214            this.e = expires;
215        }
216
217        public String getHTTPHeader() {
218            String fmt = "%s=%s; expires=%s";
219            return String.format(fmt, this.n, this.v, this.e);
220        }
221    }
222
223    /**
224     * Provides rudimentary support for cookies. Doesn't support 'path',
225     * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported
226     * features.
227     *
228     * @author LordFokas
229     */
230    public class CookieHandler implements Iterable<String> {
231
232        private final HashMap<String, String> cookies = new HashMap<String, String>();
233
234        private final ArrayList<Cookie> queue = new ArrayList<Cookie>();
235
236        public CookieHandler(Map<String, String> httpHeaders) {
237            String raw = httpHeaders.get("cookie");
238            if (raw != null) {
239                String[] tokens = raw.split(";");
240                for (String token : tokens) {
241                    String[] data = token.trim().split("=");
242                    if (data.length == 2) {
243                        this.cookies.put(data[0], data[1]);
244                    }
245                }
246            }
247        }
248
249        /**
250         * Set a cookie with an expiration date from a month ago, effectively
251         * deleting it on the client side.
252         *
253         * @param name
254         *            The cookie name.
255         */
256        public void delete(String name) {
257            set(name, "-delete-", -30);
258        }
259
260        @Override
261        public Iterator<String> iterator() {
262            return this.cookies.keySet().iterator();
263        }
264
265        /**
266         * Read a cookie from the HTTP Headers.
267         *
268         * @param name
269         *            The cookie's name.
270         * @return The cookie's value if it exists, null otherwise.
271         */
272        public String read(String name) {
273            return this.cookies.get(name);
274        }
275
276        public void set(Cookie cookie) {
277            this.queue.add(cookie);
278        }
279
280        /**
281         * Sets a cookie.
282         *
283         * @param name
284         *            The cookie's name.
285         * @param value
286         *            The cookie's value.
287         * @param expires
288         *            How many days until the cookie expires.
289         */
290        public void set(String name, String value, int expires) {
291            this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
292        }
293
294        /**
295         * Internally used by the webserver to add all queued cookies into the
296         * Response's HTTP Headers.
297         *
298         * @param response
299         *            The Response object to which headers the queued cookies
300         *            will be added.
301         */
302        public void unloadQueue(Response response) {
303            for (Cookie cookie : this.queue) {
304                response.addHeader("Set-Cookie", cookie.getHTTPHeader());
305            }
306        }
307    }
308
309    /**
310     * Default threading strategy for NanoHTTPD.
311     * <p/>
312     * <p>
313     * By default, the server spawns a new Thread for every incoming request.
314     * These are set to <i>daemon</i> status, and named according to the request
315     * number. The name is useful when profiling the application.
316     * </p>
317     */
318    public static class DefaultAsyncRunner implements AsyncRunner {
319
320        private long requestCount;
321
322        private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>());
323
324        /**
325         * @return a list with currently running clients.
326         */
327        public List<ClientHandler> getRunning() {
328            return running;
329        }
330
331        @Override
332        public void closeAll() {
333            // copy of the list for concurrency
334            for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) {
335                clientHandler.close();
336            }
337        }
338
339        @Override
340        public void closed(ClientHandler clientHandler) {
341            this.running.remove(clientHandler);
342        }
343
344        @Override
345        public void exec(ClientHandler clientHandler) {
346            ++this.requestCount;
347            Thread t = new Thread(clientHandler);
348            t.setDaemon(true);
349            t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
350            this.running.add(clientHandler);
351            t.start();
352        }
353    }
354
355    /**
356     * Default strategy for creating and cleaning up temporary files.
357     * <p/>
358     * <p>
359     * By default, files are created by <code>File.createTempFile()</code> in
360     * the directory specified.
361     * </p>
362     */
363    public static class DefaultTempFile implements TempFile {
364
365        private final File file;
366
367        private final OutputStream fstream;
368
369        public DefaultTempFile(String tempdir) throws IOException {
370            this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
371            this.fstream = new FileOutputStream(this.file);
372        }
373
374        @Override
375        public void delete() throws Exception {
376            safeClose(this.fstream);
377            if (!this.file.delete()) {
378                throw new Exception("could not delete temporary file");
379            }
380        }
381
382        @Override
383        public String getName() {
384            return this.file.getAbsolutePath();
385        }
386
387        @Override
388        public OutputStream open() throws Exception {
389            return this.fstream;
390        }
391    }
392
393    /**
394     * Default strategy for creating and cleaning up temporary files.
395     * <p/>
396     * <p>
397     * This class stores its files in the standard location (that is, wherever
398     * <code>java.io.tmpdir</code> points to). Files are added to an internal
399     * list, and deleted when no longer needed (that is, when
400     * <code>clear()</code> is invoked at the end of processing a request).
401     * </p>
402     */
403    public static class DefaultTempFileManager implements TempFileManager {
404
405        private final String tmpdir;
406
407        private final List<TempFile> tempFiles;
408
409        public DefaultTempFileManager() {
410            this.tmpdir = System.getProperty("java.io.tmpdir");
411            this.tempFiles = new ArrayList<TempFile>();
412        }
413
414        @Override
415        public void clear() {
416            for (TempFile file : this.tempFiles) {
417                try {
418                    file.delete();
419                } catch (Exception ignored) {
420                    NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
421                }
422            }
423            this.tempFiles.clear();
424        }
425
426        @Override
427        public TempFile createTempFile() throws Exception {
428            DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
429            this.tempFiles.add(tempFile);
430            return tempFile;
431        }
432    }
433
434    /**
435     * Default strategy for creating and cleaning up temporary files.
436     */
437    private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
438
439        @Override
440        public TempFileManager create() {
441            return new DefaultTempFileManager();
442        }
443    }
444
445    private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)";
446
447    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE);
448
449    private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)";
450
451    private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE);
452
453    private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]";
454
455    private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX);
456
457    protected class HTTPSession implements IHTTPSession {
458
459        public static final int BUFSIZE = 8192;
460
461        private final TempFileManager tempFileManager;
462
463        private final OutputStream outputStream;
464
465        private final PushbackInputStream inputStream;
466
467        private int splitbyte;
468
469        private int rlen;
470
471        private String uri;
472
473        private Method method;
474
475        private Map<String, String> parms;
476
477        private Map<String, String> headers;
478
479        private CookieHandler cookies;
480
481        private String queryParameterString;
482
483        private String remoteIp;
484
485        private String protocolVersion;
486
487        public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
488            this.tempFileManager = tempFileManager;
489            this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
490            this.outputStream = outputStream;
491        }
492
493        public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
494            this.tempFileManager = tempFileManager;
495            this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
496            this.outputStream = outputStream;
497            this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
498            this.headers = new HashMap<String, String>();
499        }
500
501        /**
502         * Decodes the sent headers and loads the data into Key/value pairs
503         */
504        private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) throws ResponseException {
505            try {
506                // Read the request line
507                String inLine = in.readLine();
508                if (inLine == null) {
509                    return;
510                }
511
512                StringTokenizer st = new StringTokenizer(inLine);
513                if (!st.hasMoreTokens()) {
514                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
515                }
516
517                pre.put("method", st.nextToken());
518
519                if (!st.hasMoreTokens()) {
520                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
521                }
522
523                String uri = st.nextToken();
524
525                // Decode parameters from the URI
526                int qmi = uri.indexOf('?');
527                if (qmi >= 0) {
528                    decodeParms(uri.substring(qmi + 1), parms);
529                    uri = decodePercent(uri.substring(0, qmi));
530                } else {
531                    uri = decodePercent(uri);
532                }
533
534                // If there's another token, its protocol version,
535                // followed by HTTP headers.
536                // NOTE: this now forces header names lower case since they are
537                // case insensitive and vary by client.
538                if (st.hasMoreTokens()) {
539                    protocolVersion = st.nextToken();
540                } else {
541                    protocolVersion = "HTTP/1.1";
542                    NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
543                }
544                String line = in.readLine();
545                while (line != null && line.trim().length() > 0) {
546                    int p = line.indexOf(':');
547                    if (p >= 0) {
548                        headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
549                    }
550                    line = in.readLine();
551                }
552
553                pre.put("uri", uri);
554            } catch (IOException ioe) {
555                throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
556            }
557        }
558
559        /**
560         * Decodes the Multipart Body data and put it into Key/Value pairs.
561         */
562        private void decodeMultipartFormData(String boundary, ByteBuffer fbuf, Map<String, String> parms, Map<String, String> files) throws ResponseException {
563            try {
564                int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes());
565                if (boundary_idxs.length < 2) {
566                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
567                }
568
569                final int MAX_HEADER_SIZE = 1024;
570                byte[] part_header_buff = new byte[MAX_HEADER_SIZE];
571                for (int bi = 0; bi < boundary_idxs.length - 1; bi++) {
572                    fbuf.position(boundary_idxs[bi]);
573                    int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
574                    fbuf.get(part_header_buff, 0, len);
575                    ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len);
576                    BufferedReader in = new BufferedReader(new InputStreamReader(bais));
577
578                    // First line is boundary string
579                    String mpline = in.readLine();
580                    if (!mpline.contains(boundary)) {
581                        throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
582                    }
583
584                    String part_name = null, file_name = null, content_type = null;
585                    // Parse the reset of the header lines
586                    mpline = in.readLine();
587                    while (mpline != null && mpline.trim().length() > 0) {
588                        Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline);
589                        if (matcher.matches()) {
590                            String attributeString = matcher.group(2);
591                            matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString);
592                            while (matcher.find()) {
593                                String key = matcher.group(1);
594                                if (key.equalsIgnoreCase("name")) {
595                                    part_name = matcher.group(2);
596                                } else if (key.equalsIgnoreCase("filename")) {
597                                    file_name = matcher.group(2);
598                                }
599                            }
600                        }
601                        matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
602                        if (matcher.matches()) {
603                            content_type = matcher.group(2).trim();
604                        }
605                        mpline = in.readLine();
606                    }
607
608                    // Read the part data
609                    int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE);
610                    if (part_header_len >= len - 4) {
611                        throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
612                    }
613                    int part_data_start = boundary_idxs[bi] + part_header_len;
614                    int part_data_end = boundary_idxs[bi + 1] - 4;
615
616                    fbuf.position(part_data_start);
617                    if (content_type == null) {
618                        // Read the part into a string
619                        byte[] data_bytes = new byte[part_data_end - part_data_start];
620                        fbuf.get(data_bytes);
621                        parms.put(part_name, new String(data_bytes));
622                    } else {
623                        // Read it into a file
624                        String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start);
625                        if (!files.containsKey(part_name)) {
626                            files.put(part_name, path);
627                        } else {
628                            int count = 2;
629                            while (files.containsKey(part_name + count)) {
630                                count++;
631                            }
632                            files.put(part_name + count, path);
633                        }
634                        parms.put(part_name, file_name);
635                    }
636                }
637            } catch (ResponseException re) {
638                throw re;
639            } catch (Exception e) {
640                throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString());
641            }
642        }
643
644        /**
645         * Decodes parameters in percent-encoded URI-format ( e.g.
646         * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
647         * Map. NOTE: this doesn't support multiple identical keys due to the
648         * simplicity of Map.
649         */
650        private void decodeParms(String parms, Map<String, String> p) {
651            if (parms == null) {
652                this.queryParameterString = "";
653                return;
654            }
655
656            this.queryParameterString = parms;
657            StringTokenizer st = new StringTokenizer(parms, "&");
658            while (st.hasMoreTokens()) {
659                String e = st.nextToken();
660                int sep = e.indexOf('=');
661                if (sep >= 0) {
662                    p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
663                } else {
664                    p.put(decodePercent(e).trim(), "");
665                }
666            }
667        }
668
669        @Override
670        public void execute() throws IOException {
671            try {
672                // Read the first 8192 bytes.
673                // The full header should fit in here.
674                // Apache's default header limit is 8KB.
675                // Do NOT assume that a single read will get the entire header
676                // at once!
677                byte[] buf = new byte[HTTPSession.BUFSIZE];
678                this.splitbyte = 0;
679                this.rlen = 0;
680
681                int read = -1;
682                try {
683                    read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
684                } catch (Exception e) {
685                    safeClose(this.inputStream);
686                    safeClose(this.outputStream);
687                    throw new SocketException("NanoHttpd Shutdown");
688                }
689                if (read == -1) {
690                    // socket was been closed
691                    safeClose(this.inputStream);
692                    safeClose(this.outputStream);
693                    throw new SocketException("NanoHttpd Shutdown");
694                }
695                while (read > 0) {
696                    this.rlen += read;
697                    this.splitbyte = findHeaderEnd(buf, this.rlen);
698                    if (this.splitbyte > 0) {
699                        break;
700                    }
701                    read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
702                }
703
704                if (this.splitbyte < this.rlen) {
705                    this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte);
706                }
707
708                this.parms = new HashMap<String, String>();
709                if (null == this.headers) {
710                    this.headers = new HashMap<String, String>();
711                } else {
712                    this.headers.clear();
713                }
714
715                if (null != this.remoteIp) {
716                    this.headers.put("remote-addr", this.remoteIp);
717                    this.headers.put("http-client-ip", this.remoteIp);
718                }
719
720                // Create a BufferedReader for parsing the header.
721                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
722
723                // Decode the header into parms and header java properties
724                Map<String, String> pre = new HashMap<String, String>();
725                decodeHeader(hin, pre, this.parms, this.headers);
726
727                this.method = Method.lookup(pre.get("method"));
728                if (this.method == null) {
729                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
730                }
731
732                this.uri = pre.get("uri");
733
734                this.cookies = new CookieHandler(this.headers);
735
736                String connection = this.headers.get("connection");
737                boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*"));
738
739                // Ok, now do the serve()
740                Response r = serve(this);
741                if (r == null) {
742                    throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
743                } else {
744                    String acceptEncoding = this.headers.get("accept-encoding");
745                    this.cookies.unloadQueue(r);
746                    r.setRequestMethod(this.method);
747                    r.setGzipEncoding(useGzipWhenAccepted() && acceptEncoding != null && acceptEncoding.contains("gzip"));
748                    r.setKeepAlive(keepAlive);
749                    r.send(this.outputStream);
750                }
751                if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) {
752                    throw new SocketException("NanoHttpd Shutdown");
753                }
754            } catch (SocketException e) {
755                // throw it out to close socket object (finalAccept)
756                throw e;
757            } catch (SocketTimeoutException ste) {
758                // treat socket timeouts the same way we treat socket exceptions
759                // i.e. close the stream & finalAccept object by throwing the
760                // exception up the call stack.
761                throw ste;
762            } catch (IOException ioe) {
763                Response r = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
764                r.send(this.outputStream);
765                safeClose(this.outputStream);
766            } catch (ResponseException re) {
767                Response r = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
768                r.send(this.outputStream);
769                safeClose(this.outputStream);
770            } finally {
771                this.tempFileManager.clear();
772            }
773        }
774
775        /**
776         * Find byte index separating header from body. It must be the last byte
777         * of the first two sequential new lines.
778         */
779        private int findHeaderEnd(final byte[] buf, int rlen) {
780            int splitbyte = 0;
781            while (splitbyte + 3 < rlen) {
782                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
783                    return splitbyte + 4;
784                }
785                splitbyte++;
786            }
787            return 0;
788        }
789
790        /**
791         * Find the byte positions where multipart boundaries start. This reads
792         * a large block at a time and uses a temporary buffer to optimize
793         * (memory mapped) file access.
794         */
795        private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
796            int[] res = new int[0];
797            if (b.remaining() < boundary.length) {
798                return res;
799            }
800
801            int search_window_pos = 0;
802            byte[] search_window = new byte[4 * 1024 + boundary.length];
803
804            int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length;
805            b.get(search_window, 0, first_fill);
806            int new_bytes = first_fill - boundary.length;
807
808            do {
809                // Search the search_window
810                for (int j = 0; j < new_bytes; j++) {
811                    for (int i = 0; i < boundary.length; i++) {
812                        if (search_window[j + i] != boundary[i])
813                            break;
814                        if (i == boundary.length - 1) {
815                            // Match found, add it to results
816                            int[] new_res = new int[res.length + 1];
817                            System.arraycopy(res, 0, new_res, 0, res.length);
818                            new_res[res.length] = search_window_pos + j;
819                            res = new_res;
820                        }
821                    }
822                }
823                search_window_pos += new_bytes;
824
825                // Copy the end of the buffer to the start
826                System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length);
827
828                // Refill search_window
829                new_bytes = search_window.length - boundary.length;
830                new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes;
831                b.get(search_window, boundary.length, new_bytes);
832            } while (new_bytes > 0);
833            return res;
834        }
835
836        @Override
837        public CookieHandler getCookies() {
838            return this.cookies;
839        }
840
841        @Override
842        public final Map<String, String> getHeaders() {
843            return this.headers;
844        }
845
846        @Override
847        public final InputStream getInputStream() {
848            return this.inputStream;
849        }
850
851        @Override
852        public final Method getMethod() {
853            return this.method;
854        }
855
856        @Override
857        public final Map<String, String> getParms() {
858            return this.parms;
859        }
860
861        @Override
862        public String getQueryParameterString() {
863            return this.queryParameterString;
864        }
865
866        private RandomAccessFile getTmpBucket() {
867            try {
868                TempFile tempFile = this.tempFileManager.createTempFile();
869                return new RandomAccessFile(tempFile.getName(), "rw");
870            } catch (Exception e) {
871                throw new Error(e); // we won't recover, so throw an error
872            }
873        }
874
875        @Override
876        public final String getUri() {
877            return this.uri;
878        }
879
880        @Override
881        public void parseBody(Map<String, String> files) throws IOException, ResponseException {
882            final int REQUEST_BUFFER_LEN = 512;
883            final int MEMORY_STORE_LIMIT = 1024;
884            RandomAccessFile randomAccessFile = null;
885            try {
886                long size;
887                if (this.headers.containsKey("content-length")) {
888                    size = Integer.parseInt(this.headers.get("content-length"));
889                } else if (this.splitbyte < this.rlen) {
890                    size = this.rlen - this.splitbyte;
891                } else {
892                    size = 0;
893                }
894
895                ByteArrayOutputStream baos = null;
896                DataOutput request_data_output = null;
897
898                // Store the request in memory or a file, depending on size
899                if (size < MEMORY_STORE_LIMIT) {
900                    baos = new ByteArrayOutputStream();
901                    request_data_output = new DataOutputStream(baos);
902                } else {
903                    randomAccessFile = getTmpBucket();
904                    request_data_output = randomAccessFile;
905                }
906
907                // Read all the body and write it to request_data_output
908                byte[] buf = new byte[REQUEST_BUFFER_LEN];
909                while (this.rlen >= 0 && size > 0) {
910                    this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN));
911                    size -= this.rlen;
912                    if (this.rlen > 0) {
913                        request_data_output.write(buf, 0, this.rlen);
914                    }
915                }
916
917                ByteBuffer fbuf = null;
918                if (baos != null) {
919                    fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size());
920                } else {
921                    fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
922                    randomAccessFile.seek(0);
923                }
924
925                // If the method is POST, there may be parameters
926                // in data section, too, read it:
927                if (Method.POST.equals(this.method)) {
928                    String contentType = "";
929                    String contentTypeHeader = this.headers.get("content-type");
930
931                    StringTokenizer st = null;
932                    if (contentTypeHeader != null) {
933                        st = new StringTokenizer(contentTypeHeader, ",; ");
934                        if (st.hasMoreTokens()) {
935                            contentType = st.nextToken();
936                        }
937                    }
938
939                    if ("multipart/form-data".equalsIgnoreCase(contentType)) {
940                        // Handle multipart/form-data
941                        if (!st.hasMoreTokens()) {
942                            throw new ResponseException(Response.Status.BAD_REQUEST,
943                                    "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
944                        }
945
946                        String boundaryStartString = "boundary=";
947                        int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
948                        String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
949                        if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
950                            boundary = boundary.substring(1, boundary.length() - 1);
951                        }
952
953                        decodeMultipartFormData(boundary, fbuf, this.parms, files);
954                    } else {
955                        byte[] postBytes = new byte[fbuf.remaining()];
956                        fbuf.get(postBytes);
957                        String postLine = new String(postBytes).trim();
958                        // Handle application/x-www-form-urlencoded
959                        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
960                            decodeParms(postLine, this.parms);
961                        } else if (postLine.length() != 0) {
962                            // Special case for raw POST data => create a
963                            // special files entry "postData" with raw content
964                            // data
965                            files.put("postData", postLine);
966                        }
967                    }
968                } else if (Method.PUT.equals(this.method)) {
969                    files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
970                }
971            } finally {
972                safeClose(randomAccessFile);
973            }
974        }
975
976        /**
977         * Retrieves the content of a sent file and saves it to a temporary
978         * file. The full path to the saved file is returned.
979         */
980        private String saveTmpFile(ByteBuffer b, int offset, int len) {
981            String path = "";
982            if (len > 0) {
983                FileOutputStream fileOutputStream = null;
984                try {
985                    TempFile tempFile = this.tempFileManager.createTempFile();
986                    ByteBuffer src = b.duplicate();
987                    fileOutputStream = new FileOutputStream(tempFile.getName());
988                    FileChannel dest = fileOutputStream.getChannel();
989                    src.position(offset).limit(offset + len);
990                    dest.write(src.slice());
991                    path = tempFile.getName();
992                } catch (Exception e) { // Catch exception if any
993                    throw new Error(e); // we won't recover, so throw an error
994                } finally {
995                    safeClose(fileOutputStream);
996                }
997            }
998            return path;
999        }
1000    }
1001
1002    /**
1003     * Handles one session, i.e. parses the HTTP request and returns the
1004     * response.
1005     */
1006    public interface IHTTPSession {
1007
1008        void execute() throws IOException;
1009
1010        CookieHandler getCookies();
1011
1012        Map<String, String> getHeaders();
1013
1014        InputStream getInputStream();
1015
1016        Method getMethod();
1017
1018        Map<String, String> getParms();
1019
1020        String getQueryParameterString();
1021
1022        /**
1023         * @return the path part of the URL.
1024         */
1025        String getUri();
1026
1027        /**
1028         * Adds the files in the request body to the files map.
1029         *
1030         * @param files
1031         *            map to modify
1032         */
1033        void parseBody(Map<String, String> files) throws IOException, ResponseException;
1034    }
1035
1036    /**
1037     * HTTP Request methods, with the ability to decode a <code>String</code>
1038     * back to its enum value.
1039     */
1040    public enum Method {
1041        GET,
1042        PUT,
1043        POST,
1044        DELETE,
1045        HEAD,
1046        OPTIONS;
1047
1048        static Method lookup(String method) {
1049            for (Method m : Method.values()) {
1050                if (m.toString().equalsIgnoreCase(method)) {
1051                    return m;
1052                }
1053            }
1054            return null;
1055        }
1056    }
1057
1058    /**
1059     * HTTP response. Return one of these from serve().
1060     */
1061    public static class Response {
1062
1063        public interface IStatus {
1064
1065            String getDescription();
1066
1067            int getRequestStatus();
1068        }
1069
1070        /**
1071         * Some HTTP response status codes
1072         */
1073        public enum Status implements IStatus {
1074            SWITCH_PROTOCOL(101, "Switching Protocols"),
1075            OK(200, "OK"),
1076            CREATED(201, "Created"),
1077            ACCEPTED(202, "Accepted"),
1078            NO_CONTENT(204, "No Content"),
1079            PARTIAL_CONTENT(206, "Partial Content"),
1080            REDIRECT(301, "Moved Permanently"),
1081            NOT_MODIFIED(304, "Not Modified"),
1082            BAD_REQUEST(400, "Bad Request"),
1083            UNAUTHORIZED(401, "Unauthorized"),
1084            FORBIDDEN(403, "Forbidden"),
1085            NOT_FOUND(404, "Not Found"),
1086            METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
1087            RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
1088            INTERNAL_ERROR(500, "Internal Server Error"),
1089            UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
1090
1091            private final int requestStatus;
1092
1093            private final String description;
1094
1095            Status(int requestStatus, String description) {
1096                this.requestStatus = requestStatus;
1097                this.description = description;
1098            }
1099
1100            @Override
1101            public String getDescription() {
1102                return "" + this.requestStatus + " " + this.description;
1103            }
1104
1105            @Override
1106            public int getRequestStatus() {
1107                return this.requestStatus;
1108            }
1109
1110        }
1111
1112        /**
1113         * Output stream that will automatically send every write to the wrapped
1114         * OutputStream according to chunked transfer:
1115         * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
1116         */
1117        private static class ChunkedOutputStream extends FilterOutputStream {
1118
1119            public ChunkedOutputStream(OutputStream out) {
1120                super(out);
1121            }
1122
1123            @Override
1124            public void write(int b) throws IOException {
1125                byte[] data = {
1126                    (byte) b
1127                };
1128                write(data, 0, 1);
1129            }
1130
1131            @Override
1132            public void write(byte[] b) throws IOException {
1133                write(b, 0, b.length);
1134            }
1135
1136            @Override
1137            public void write(byte[] b, int off, int len) throws IOException {
1138                if (len == 0)
1139                    return;
1140                out.write(String.format("%x\r\n", len).getBytes());
1141                out.write(b, off, len);
1142                out.write("\r\n".getBytes());
1143            }
1144
1145            public void finish() throws IOException {
1146                out.write("0\r\n\r\n".getBytes());
1147            }
1148
1149        }
1150
1151        /**
1152         * HTTP status code after processing, e.g. "200 OK", Status.OK
1153         */
1154        private IStatus status;
1155
1156        /**
1157         * MIME type of content, e.g. "text/html"
1158         */
1159        private String mimeType;
1160
1161        /**
1162         * Data of the response, may be null.
1163         */
1164        private InputStream data;
1165
1166        private long contentLength;
1167
1168        /**
1169         * Headers for the HTTP response. Use addHeader() to add lines.
1170         */
1171        private final Map<String, String> header = new HashMap<String, String>();
1172
1173        /**
1174         * The request method that spawned this response.
1175         */
1176        private Method requestMethod;
1177
1178        /**
1179         * Use chunkedTransfer
1180         */
1181        private boolean chunkedTransfer;
1182
1183        private boolean encodeAsGzip;
1184
1185        private boolean keepAlive;
1186
1187        /**
1188         * Creates a fixed length response if totalBytes>=0, otherwise chunked.
1189         */
1190        protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
1191            this.status = status;
1192            this.mimeType = mimeType;
1193            if (data == null) {
1194                this.data = new ByteArrayInputStream(new byte[0]);
1195                this.contentLength = 0L;
1196            } else {
1197                this.data = data;
1198                this.contentLength = totalBytes;
1199            }
1200            this.chunkedTransfer = this.contentLength < 0;
1201            keepAlive = true;
1202        }
1203
1204        /**
1205         * Adds given line to the header.
1206         */
1207        public void addHeader(String name, String value) {
1208            this.header.put(name, value);
1209        }
1210
1211        public InputStream getData() {
1212            return this.data;
1213        }
1214
1215        public String getHeader(String name) {
1216            for (String headerName : header.keySet()) {
1217                if (headerName.equalsIgnoreCase(name)) {
1218                    return header.get(headerName);
1219                }
1220            }
1221            return null;
1222        }
1223
1224        public String getMimeType() {
1225            return this.mimeType;
1226        }
1227
1228        public Method getRequestMethod() {
1229            return this.requestMethod;
1230        }
1231
1232        public IStatus getStatus() {
1233            return this.status;
1234        }
1235
1236        public void setGzipEncoding(boolean encodeAsGzip) {
1237            this.encodeAsGzip = encodeAsGzip;
1238        }
1239
1240        public void setKeepAlive(boolean useKeepAlive) {
1241            this.keepAlive = useKeepAlive;
1242        }
1243
1244        private boolean headerAlreadySent(Map<String, String> header, String name) {
1245            boolean alreadySent = false;
1246            for (String headerName : header.keySet()) {
1247                alreadySent |= headerName.equalsIgnoreCase(name);
1248            }
1249            return alreadySent;
1250        }
1251
1252        /**
1253         * Sends given response to the socket.
1254         */
1255        protected void send(OutputStream outputStream) {
1256            String mime = this.mimeType;
1257            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1258            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1259
1260            try {
1261                if (this.status == null) {
1262                    throw new Error("sendResponse(): Status can't be null.");
1263                }
1264                PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false);
1265                pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n");
1266
1267                if (mime != null) {
1268                    pw.print("Content-Type: " + mime + "\r\n");
1269                }
1270
1271                if (this.header == null || this.header.get("Date") == null) {
1272                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
1273                }
1274
1275                if (this.header != null) {
1276                    for (String key : this.header.keySet()) {
1277                        String value = this.header.get(key);
1278                        pw.print(key + ": " + value + "\r\n");
1279                    }
1280                }
1281
1282                if (!headerAlreadySent(header, "connection")) {
1283                    pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n");
1284                }
1285
1286                if (headerAlreadySent(this.header, "content-length")) {
1287                    encodeAsGzip = false;
1288                }
1289
1290                if (encodeAsGzip) {
1291                    pw.print("Content-Encoding: gzip\r\n");
1292                    setChunkedTransfer(true);
1293                }
1294
1295                long pending = this.data != null ? this.contentLength : 0;
1296                if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
1297                    pw.print("Transfer-Encoding: chunked\r\n");
1298                } else if (!encodeAsGzip) {
1299                    pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
1300                }
1301                pw.print("\r\n");
1302                pw.flush();
1303                sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
1304                outputStream.flush();
1305                safeClose(this.data);
1306            } catch (IOException ioe) {
1307                NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
1308            }
1309        }
1310
1311        private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
1312            if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
1313                ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
1314                sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
1315                chunkedOutputStream.finish();
1316            } else {
1317                sendBodyWithCorrectEncoding(outputStream, pending);
1318            }
1319        }
1320
1321        private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
1322            if (encodeAsGzip) {
1323                GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
1324                sendBody(gzipOutputStream, -1);
1325                gzipOutputStream.finish();
1326            } else {
1327                sendBody(outputStream, pending);
1328            }
1329        }
1330
1331        /**
1332         * Sends the body to the specified OutputStream. The pending parameter
1333         * limits the maximum amounts of bytes sent unless it is -1, in which
1334         * case everything is sent.
1335         *
1336         * @param outputStream
1337         *            the OutputStream to send data to
1338         * @param pending
1339         *            -1 to send everything, otherwise sets a max limit to the
1340         *            number of bytes sent
1341         * @throws IOException
1342         *             if something goes wrong while sending the data.
1343         */
1344        private void sendBody(OutputStream outputStream, long pending) throws IOException {
1345            long BUFFER_SIZE = 16 * 1024;
1346            byte[] buff = new byte[(int) BUFFER_SIZE];
1347            boolean sendEverything = pending == -1;
1348            while (pending > 0 || sendEverything) {
1349                long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
1350                int read = this.data.read(buff, 0, (int) bytesToRead);
1351                if (read <= 0) {
1352                    break;
1353                }
1354                outputStream.write(buff, 0, read);
1355                if (!sendEverything) {
1356                    pending -= read;
1357                }
1358            }
1359        }
1360
1361        protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) {
1362            for (String headerName : header.keySet()) {
1363                if (headerName.equalsIgnoreCase("content-length")) {
1364                    try {
1365                        return Long.parseLong(header.get(headerName));
1366                    } catch (NumberFormatException ex) {
1367                        return size;
1368                    }
1369                }
1370            }
1371
1372            pw.print("Content-Length: " + size + "\r\n");
1373            return size;
1374        }
1375
1376        public void setChunkedTransfer(boolean chunkedTransfer) {
1377            this.chunkedTransfer = chunkedTransfer;
1378        }
1379
1380        public void setData(InputStream data) {
1381            this.data = data;
1382        }
1383
1384        public void setMimeType(String mimeType) {
1385            this.mimeType = mimeType;
1386        }
1387
1388        public void setRequestMethod(Method requestMethod) {
1389            this.requestMethod = requestMethod;
1390        }
1391
1392        public void setStatus(IStatus status) {
1393            this.status = status;
1394        }
1395    }
1396
1397    public static final class ResponseException extends Exception {
1398
1399        private static final long serialVersionUID = 6569838532917408380L;
1400
1401        private final Response.Status status;
1402
1403        public ResponseException(Response.Status status, String message) {
1404            super(message);
1405            this.status = status;
1406        }
1407
1408        public ResponseException(Response.Status status, String message, Exception e) {
1409            super(message, e);
1410            this.status = status;
1411        }
1412
1413        public Response.Status getStatus() {
1414            return this.status;
1415        }
1416    }
1417
1418    /**
1419     * The runnable that will be used for the main listening thread.
1420     */
1421    public class ServerRunnable implements Runnable {
1422
1423        private final int timeout;
1424
1425        private IOException bindException;
1426
1427        private boolean hasBinded = false;
1428
1429        private ServerRunnable(int timeout) {
1430            this.timeout = timeout;
1431        }
1432
1433        @Override
1434        public void run() {
1435            try {
1436                myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
1437                hasBinded = true;
1438            } catch (IOException e) {
1439                this.bindException = e;
1440                return;
1441            }
1442            do {
1443                try {
1444                    final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
1445                    if (this.timeout > 0) {
1446                        finalAccept.setSoTimeout(this.timeout);
1447                    }
1448                    final InputStream inputStream = finalAccept.getInputStream();
1449                    NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
1450                } catch (IOException e) {
1451                    NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
1452                }
1453            } while (!NanoHTTPD.this.myServerSocket.isClosed());
1454        }
1455    }
1456
1457    /**
1458     * A temp file.
1459     * <p/>
1460     * <p>
1461     * Temp files are responsible for managing the actual temporary storage and
1462     * cleaning themselves up when no longer needed.
1463     * </p>
1464     */
1465    public interface TempFile {
1466
1467        void delete() throws Exception;
1468
1469        String getName();
1470
1471        OutputStream open() throws Exception;
1472    }
1473
1474    /**
1475     * Temp file manager.
1476     * <p/>
1477     * <p>
1478     * Temp file managers are created 1-to-1 with incoming requests, to create
1479     * and cleanup temporary files created as a result of handling the request.
1480     * </p>
1481     */
1482    public interface TempFileManager {
1483
1484        void clear();
1485
1486        TempFile createTempFile() throws Exception;
1487    }
1488
1489    /**
1490     * Factory to create temp file managers.
1491     */
1492    public interface TempFileManagerFactory {
1493
1494        TempFileManager create();
1495    }
1496
1497    /**
1498     * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
1499     * This is required as the Keep-Alive HTTP connections would otherwise block
1500     * the socket reading thread forever (or as long the browser is open).
1501     */
1502    public static final int SOCKET_READ_TIMEOUT = 5000;
1503
1504    /**
1505     * Common MIME type for dynamic content: plain text
1506     */
1507    public static final String MIME_PLAINTEXT = "text/plain";
1508
1509    /**
1510     * Common MIME type for dynamic content: html
1511     */
1512    public static final String MIME_HTML = "text/html";
1513
1514    /**
1515     * Pseudo-Parameter to use to store the actual query string in the
1516     * parameters map for later re-processing.
1517     */
1518    private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
1519
1520    /**
1521     * logger to log to.
1522     */
1523    private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
1524
1525    /**
1526     * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
1527     * array of loaded KeyManagers. These objects must properly
1528     * loaded/initialized by the caller.
1529     */
1530    public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
1531        SSLServerSocketFactory res = null;
1532        try {
1533            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1534            trustManagerFactory.init(loadedKeyStore);
1535            SSLContext ctx = SSLContext.getInstance("TLS");
1536            ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
1537            res = ctx.getServerSocketFactory();
1538        } catch (Exception e) {
1539            throw new IOException(e.getMessage());
1540        }
1541        return res;
1542    }
1543
1544    /**
1545     * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
1546     * loaded KeyManagerFactory. These objects must properly loaded/initialized
1547     * by the caller.
1548     */
1549    public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
1550        SSLServerSocketFactory res = null;
1551        try {
1552            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1553            trustManagerFactory.init(loadedKeyStore);
1554            SSLContext ctx = SSLContext.getInstance("TLS");
1555            ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
1556            res = ctx.getServerSocketFactory();
1557        } catch (Exception e) {
1558            throw new IOException(e.getMessage());
1559        }
1560        return res;
1561    }
1562
1563    /**
1564     * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
1565     * certificate and passphrase
1566     */
1567    public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
1568        SSLServerSocketFactory res = null;
1569        try {
1570            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
1571            InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
1572            keystore.load(keystoreStream, passphrase);
1573            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1574            trustManagerFactory.init(keystore);
1575            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
1576            keyManagerFactory.init(keystore, passphrase);
1577            SSLContext ctx = SSLContext.getInstance("TLS");
1578            ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
1579            res = ctx.getServerSocketFactory();
1580        } catch (Exception e) {
1581            throw new IOException(e.getMessage());
1582        }
1583        return res;
1584    }
1585
1586    private static final void safeClose(Object closeable) {
1587        try {
1588            if (closeable != null) {
1589                if (closeable instanceof Closeable) {
1590                    ((Closeable) closeable).close();
1591                } else if (closeable instanceof Socket) {
1592                    ((Socket) closeable).close();
1593                } else if (closeable instanceof ServerSocket) {
1594                    ((ServerSocket) closeable).close();
1595                } else {
1596                    throw new IllegalArgumentException("Unknown object to close");
1597                }
1598            }
1599        } catch (IOException e) {
1600            NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
1601        }
1602    }
1603
1604    private final String hostname;
1605
1606    private final int myPort;
1607
1608    private ServerSocket myServerSocket;
1609
1610    private SSLServerSocketFactory sslServerSocketFactory;
1611
1612    private Thread myThread;
1613
1614    /**
1615     * Pluggable strategy for asynchronously executing requests.
1616     */
1617    protected AsyncRunner asyncRunner;
1618
1619    /**
1620     * Pluggable strategy for creating and cleaning up temporary files.
1621     */
1622    private TempFileManagerFactory tempFileManagerFactory;
1623
1624    /**
1625     * Constructs an HTTP server on given port.
1626     */
1627    public NanoHTTPD(int port) {
1628        this(null, port);
1629    }
1630
1631    // -------------------------------------------------------------------------------
1632    // //
1633    //
1634    // Threading Strategy.
1635    //
1636    // -------------------------------------------------------------------------------
1637    // //
1638
1639    /**
1640     * Constructs an HTTP server on given hostname and port.
1641     */
1642    public NanoHTTPD(String hostname, int port) {
1643        this.hostname = hostname;
1644        this.myPort = port;
1645        setTempFileManagerFactory(new DefaultTempFileManagerFactory());
1646        setAsyncRunner(new DefaultAsyncRunner());
1647    }
1648
1649    /**
1650     * Forcibly closes all connections that are open.
1651     */
1652    public synchronized void closeAllConnections() {
1653        stop();
1654    }
1655
1656    /**
1657     * create a instance of the client handler, subclasses can return a subclass
1658     * of the ClientHandler.
1659     *
1660     * @param finalAccept
1661     *            the socket the cleint is connected to
1662     * @param inputStream
1663     *            the input stream
1664     * @return the client handler
1665     */
1666    protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
1667        return new ClientHandler(inputStream, finalAccept);
1668    }
1669
1670    /**
1671     * Instantiate the server runnable, can be overwritten by subclasses to
1672     * provide a subclass of the ServerRunnable.
1673     *
1674     * @param timeout
1675     *            the socet timeout to use.
1676     * @return the server runnable.
1677     */
1678    protected ServerRunnable createServerRunnable(final int timeout) {
1679        return new ServerRunnable(timeout);
1680    }
1681
1682    /**
1683     * Decode parameters from a URL, handing the case where a single parameter
1684     * name might have been supplied several times, by return lists of values.
1685     * In general these lists will contain a single element.
1686     *
1687     * @param parms
1688     *            original <b>NanoHTTPD</b> parameters values, as passed to the
1689     *            <code>serve()</code> method.
1690     * @return a map of <code>String</code> (parameter name) to
1691     *         <code>List&lt;String&gt;</code> (a list of the values supplied).
1692     */
1693    protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
1694        return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER));
1695    }
1696
1697    // -------------------------------------------------------------------------------
1698    // //
1699
1700    /**
1701     * Decode parameters from a URL, handing the case where a single parameter
1702     * name might have been supplied several times, by return lists of values.
1703     * In general these lists will contain a single element.
1704     *
1705     * @param queryString
1706     *            a query string pulled from the URL.
1707     * @return a map of <code>String</code> (parameter name) to
1708     *         <code>List&lt;String&gt;</code> (a list of the values supplied).
1709     */
1710    protected Map<String, List<String>> decodeParameters(String queryString) {
1711        Map<String, List<String>> parms = new HashMap<String, List<String>>();
1712        if (queryString != null) {
1713            StringTokenizer st = new StringTokenizer(queryString, "&");
1714            while (st.hasMoreTokens()) {
1715                String e = st.nextToken();
1716                int sep = e.indexOf('=');
1717                String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
1718                if (!parms.containsKey(propertyName)) {
1719                    parms.put(propertyName, new ArrayList<String>());
1720                }
1721                String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
1722                if (propertyValue != null) {
1723                    parms.get(propertyName).add(propertyValue);
1724                }
1725            }
1726        }
1727        return parms;
1728    }
1729
1730    /**
1731     * Decode percent encoded <code>String</code> values.
1732     *
1733     * @param str
1734     *            the percent encoded <code>String</code>
1735     * @return expanded form of the input, for example "foo%20bar" becomes
1736     *         "foo bar"
1737     */
1738    protected String decodePercent(String str) {
1739        String decoded = null;
1740        try {
1741            decoded = URLDecoder.decode(str, "UTF8");
1742        } catch (UnsupportedEncodingException ignored) {
1743            NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
1744        }
1745        return decoded;
1746    }
1747
1748    /**
1749     * @return true if the gzip compression should be used if the client
1750     *         accespts it.
1751     */
1752    protected boolean useGzipWhenAccepted() {
1753        return true;
1754    }
1755
1756    public final int getListeningPort() {
1757        return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
1758    }
1759
1760    public final boolean isAlive() {
1761        return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
1762    }
1763
1764    /**
1765     * Call before start() to serve over HTTPS instead of HTTP
1766     */
1767    public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) {
1768        this.sslServerSocketFactory = sslServerSocketFactory;
1769    }
1770
1771    /**
1772     * Create a response with unknown length (using HTTP 1.1 chunking).
1773     */
1774    public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) {
1775        return new Response(status, mimeType, data, -1);
1776    }
1777
1778    /**
1779     * Create a response with known length.
1780     */
1781    public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) {
1782        return new Response(status, mimeType, data, totalBytes);
1783    }
1784
1785    /**
1786     * Create a text response with known length.
1787     */
1788    public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) {
1789        if (txt == null) {
1790            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
1791        } else {
1792            byte[] bytes;
1793            try {
1794                bytes = txt.getBytes("UTF-8");
1795            } catch (UnsupportedEncodingException e) {
1796                NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
1797                bytes = new byte[0];
1798            }
1799            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length);
1800        }
1801    }
1802
1803    /**
1804     * Create a text response with known length.
1805     */
1806    public Response newFixedLengthResponse(String msg) {
1807        return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
1808    }
1809
1810    /**
1811     * Override this to customize the server.
1812     * <p/>
1813     * <p/>
1814     * (By default, this returns a 404 "Not Found" plain text error response.)
1815     *
1816     * @param session
1817     *            The HTTP session
1818     * @return HTTP response, see class Response for details
1819     */
1820    public Response serve(IHTTPSession session) {
1821        Map<String, String> files = new HashMap<String, String>();
1822        Method method = session.getMethod();
1823        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
1824            try {
1825                session.parseBody(files);
1826            } catch (IOException ioe) {
1827                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
1828            } catch (ResponseException re) {
1829                return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
1830            }
1831        }
1832
1833        Map<String, String> parms = session.getParms();
1834        parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
1835        return serve(session.getUri(), method, session.getHeaders(), parms, files);
1836    }
1837
1838    /**
1839     * Override this to customize the server.
1840     * <p/>
1841     * <p/>
1842     * (By default, this returns a 404 "Not Found" plain text error response.)
1843     *
1844     * @param uri
1845     *            Percent-decoded URI without parameters, for example
1846     *            "/index.cgi"
1847     * @param method
1848     *            "GET", "POST" etc.
1849     * @param parms
1850     *            Parsed, percent decoded parameters from URI and, in case of
1851     *            POST, data.
1852     * @param headers
1853     *            Header entries, percent decoded
1854     * @return HTTP response, see class Response for details
1855     */
1856    @Deprecated
1857    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
1858        return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
1859    }
1860
1861    /**
1862     * Pluggable strategy for asynchronously executing requests.
1863     *
1864     * @param asyncRunner
1865     *            new strategy for handling threads.
1866     */
1867    public void setAsyncRunner(AsyncRunner asyncRunner) {
1868        this.asyncRunner = asyncRunner;
1869    }
1870
1871    /**
1872     * Pluggable strategy for creating and cleaning up temporary files.
1873     *
1874     * @param tempFileManagerFactory
1875     *            new strategy for handling temp files.
1876     */
1877    public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
1878        this.tempFileManagerFactory = tempFileManagerFactory;
1879    }
1880
1881    /**
1882     * Start the server.
1883     *
1884     * @throws IOException
1885     *             if the socket is in use.
1886     */
1887    public void start() throws IOException {
1888        start(NanoHTTPD.SOCKET_READ_TIMEOUT);
1889    }
1890
1891    /**
1892     * Start the server.
1893     *
1894     * @param timeout
1895     *            timeout to use for socket connections.
1896     * @throws IOException
1897     *             if the socket is in use.
1898     */
1899    public void start(final int timeout) throws IOException {
1900        if (this.sslServerSocketFactory != null) {
1901            SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
1902            ss.setNeedClientAuth(false);
1903            this.myServerSocket = ss;
1904        } else {
1905            this.myServerSocket = new ServerSocket();
1906        }
1907        this.myServerSocket.setReuseAddress(true);
1908
1909        ServerRunnable serverRunnable = createServerRunnable(timeout);
1910        this.myThread = new Thread(serverRunnable);
1911        this.myThread.setDaemon(true);
1912        this.myThread.setName("NanoHttpd Main Listener");
1913        this.myThread.start();
1914        while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
1915            try {
1916                Thread.sleep(10L);
1917            } catch (Throwable e) {
1918                // on android this may not be allowed, that's why we
1919                // catch throwable the wait should be very short because we are
1920                // just waiting for the bind of the socket
1921            }
1922        }
1923        if (serverRunnable.bindException != null) {
1924            throw serverRunnable.bindException;
1925        }
1926    }
1927
1928    /**
1929     * Stop the server.
1930     */
1931    public void stop() {
1932        try {
1933            safeClose(this.myServerSocket);
1934            this.asyncRunner.closeAll();
1935            if (this.myThread != null) {
1936                this.myThread.join();
1937            }
1938        } catch (Exception e) {
1939            NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
1940        }
1941    }
1942
1943    public final boolean wasStarted() {
1944        return this.myServerSocket != null && this.myThread != null;
1945    }
1946}
1947