NanoHTTPD.java revision 269baff79590de5abd70e8e05bf46547e4a28ee6
1package fi.iki.elonen;
2
3import java.io.BufferedReader;
4import java.io.ByteArrayInputStream;
5import java.io.ByteArrayOutputStream;
6import java.io.File;
7import java.io.FileInputStream;
8import java.io.FileOutputStream;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.InputStreamReader;
12import java.io.OutputStream;
13import java.io.PrintStream;
14import java.io.PrintWriter;
15import java.net.ServerSocket;
16import java.net.Socket;
17import java.net.URLEncoder;
18import java.util.ArrayList;
19import java.util.Date;
20import java.util.HashMap;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Locale;
24import java.util.Map;
25import java.util.Properties;
26import java.util.StringTokenizer;
27import java.util.TimeZone;
28
29/**
30 * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
31 *
32 * <p>
33 * NanoHTTPD version 1.25, Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) and Copyright &copy; 2010
34 * Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
35 *
36 * <p>
37 * <b>Features + limitations: </b>
38 * <ul>
39 *
40 * <li>Only one Java file</li>
41 * <li>Java 1.1 compatible</li>
42 * <li>Released as open source, Modified BSD licence</li>
43 * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
44 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
45 * <li>Supports both dynamic content and file serving</li>
46 * <li>Supports file upload (since version 1.2, 2010)</li>
47 * <li>Supports partial content (streaming)</li>
48 * <li>Supports ETags</li>
49 * <li>Never caches anything</li>
50 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
51 * <li>Default code serves files and shows all HTTP parameters and headers</li>
52 * <li>File server supports directory listing, index.html and index.htm</li>
53 * <li>File server supports partial content (streaming)</li>
54 * <li>File server supports ETags</li>
55 * <li>File server does the 301 redirection trick for directories without '/'</li>
56 * <li>File server supports simple skipping for files (continue download)</li>
57 * <li>File server serves also very long files without memory overhead</li>
58 * <li>Contains a built-in list of most common mime types</li>
59 * <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
60 *
61 * </ul>
62 *
63 * <p>
64 * <b>Ways to use: </b>
65 * <ul>
66 *
67 * <li>Run as a standalone app, serves files and shows requests</li>
68 * <li>Subclass serve() and embed to your own program</li>
69 * <li>Call serveFile() from serve() with your own base directory</li>
70 *
71 * </ul>
72 *
73 * See the end of the source file for distribution license (Modified BSD licence)
74 */
75public class NanoHTTPD {
76    // ==================================================
77    // API parts
78    // ==================================================
79
80    /**
81     * Override this to customize the server.
82     * <p>
83     *
84     * (By default, this delegates to serveFile() and allows directory listing.)
85     *
86     * @param uri
87     *            Percent-decoded URI without parameters, for example "/index.cgi"
88     * @param method
89     *            "GET", "POST" etc.
90     * @param parms
91     *            Parsed, percent decoded parameters from URI and, in case of POST, data.
92     * @param header
93     *            Header entries, percent decoded
94     * @return HTTP response, see class Response for details
95     */
96    public Response serve(String uri, String method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {
97        myOut.println(method + " '" + uri + "' ");
98
99        Iterator<String> e = header.keySet().iterator();
100        while (e.hasNext()) {
101            String value = e.next();
102            myOut.println("  HDR: '" + value + "' = '" + header.get(value) + "'");
103        }
104        e = parms.keySet().iterator();
105        while (e.hasNext()) {
106            String value = e.next();
107            myOut.println("  PRM: '" + value + "' = '" + parms.get(value) + "'");
108        }
109        e = files.keySet().iterator();
110        while (e.hasNext()) {
111            String value = e.next();
112            myOut.println("  UPLOADED: '" + value + "' = '" + files.get(value) + "'");
113        }
114
115        return serveFile(uri, header, myRootDir, true);
116    }
117
118    /**
119     * HTTP response. Return one of these from serve().
120     */
121    public class Response {
122        /**
123         * Default constructor: response = HTTP_OK, data = mime = 'null'
124         */
125        public Response() {
126            this.status = HTTP_OK;
127        }
128
129        /**
130         * Basic constructor.
131         */
132        public Response(String status, String mimeType, InputStream data) {
133            this.status = status;
134            this.mimeType = mimeType;
135            this.data = data;
136        }
137
138        /**
139         * Convenience method that makes an InputStream out of given text.
140         */
141        public Response(String status, String mimeType, String txt) {
142            this.status = status;
143            this.mimeType = mimeType;
144            try {
145                this.data = new ByteArrayInputStream(txt.getBytes("UTF-8"));
146            } catch (java.io.UnsupportedEncodingException uee) {
147                uee.printStackTrace();
148            }
149        }
150
151        /**
152         * Adds given line to the header.
153         */
154        public void addHeader(String name, String value) {
155            header.put(name, value);
156        }
157
158        /**
159         * HTTP status code after processing, e.g. "200 OK", HTTP_OK
160         */
161        public String status;
162
163        /**
164         * MIME type of content, e.g. "text/html"
165         */
166        public String mimeType;
167
168        /**
169         * Data of the response, may be null.
170         */
171        public InputStream data;
172
173        /**
174         * Headers for the HTTP response. Use addHeader() to add lines.
175         */
176        public Map<String, String> header = new HashMap<String, String>();
177    }
178
179    /**
180     * Some HTTP response status codes
181     */
182    public static final String HTTP_OK = "200 OK";
183    public static final String HTTP_PARTIALCONTENT = "206 Partial Content";
184    public static final String HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable";
185    public static final String HTTP_REDIRECT = "301 Moved Permanently";
186    public static final String HTTP_NOTMODIFIED = "304 Not Modified";
187    public static final String HTTP_FORBIDDEN = "403 Forbidden";
188    public static final String HTTP_NOTFOUND = "404 Not Found";
189    public static final String HTTP_BADREQUEST = "400 Bad Request";
190    public static final String HTTP_INTERNALERROR = "500 Internal Server Error";
191    public static final String HTTP_NOTIMPLEMENTED = "501 Not Implemented";
192
193    /**
194     * Common mime types for dynamic content
195     */
196    public static final String MIME_PLAINTEXT = "text/plain";
197    public static final String MIME_HTML = "text/html";
198    public static final String MIME_DEFAULT_BINARY = "application/octet-stream";
199    public static final String MIME_XML = "text/xml";
200
201    // ==================================================
202    // Socket & server code
203    // ==================================================
204
205    /**
206     * Starts a HTTP server to given port.
207     * <p>
208     * Throws an IOException if the socket is already in use
209     */
210    public NanoHTTPD(int port, File wwwroot) throws IOException {
211        myTcpPort = port;
212        this.myRootDir = wwwroot;
213        myServerSocket = new ServerSocket(myTcpPort);
214        myThread = new Thread(new Runnable() {
215            @Override
216            public void run() {
217                try {
218                    while (true) {
219                        new HTTPSession(myServerSocket.accept());
220                    }
221                } catch (IOException ioe) {
222                }
223            }
224        });
225        myThread.setDaemon(true);
226        myThread.start();
227    }
228
229    /**
230     * Stops the server.
231     */
232    public void stop() {
233        try {
234            myServerSocket.close();
235            myThread.join();
236        } catch (IOException ioe) {
237        } catch (InterruptedException e) {
238        }
239    }
240
241    /**
242     * Starts as a standalone file server and waits for Enter.
243     */
244    public static void main(String[] args) {
245        myOut.println("NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n"
246                + "(Command line options: [-p port] [-d root-dir] [--licence])\n");
247
248        // Defaults
249        int port = 80;
250        File wwwroot = new File(".").getAbsoluteFile();
251
252        // Show licence if requested
253        for (int i = 0; i < args.length; ++i)
254            if (args[i].equalsIgnoreCase("-p"))
255                port = Integer.parseInt(args[i + 1]);
256            else if (args[i].equalsIgnoreCase("-d"))
257                wwwroot = new File(args[i + 1]).getAbsoluteFile();
258            else if (args[i].toLowerCase().endsWith("licence")) {
259                myOut.println(LICENCE + "\n");
260                break;
261            }
262
263        try {
264            new NanoHTTPD(port, wwwroot);
265        } catch (IOException ioe) {
266            myErr.println("Couldn't start server:\n" + ioe);
267            System.exit(-1);
268        }
269
270        myOut.println("Now serving files in port " + port + " from \"" + wwwroot + "\"");
271        myOut.println("Hit Enter to stop.\n");
272
273        try {
274            System.in.read();
275        } catch (Throwable t) {
276        }
277    }
278
279    /**
280     * Handles one session, i.e. parses the HTTP request and returns the response.
281     */
282    private class HTTPSession implements Runnable {
283        public HTTPSession(Socket s) {
284            mySocket = s;
285            Thread t = new Thread(this);
286            t.setDaemon(true);
287            t.start();
288        }
289
290        @Override
291        public void run() {
292            try {
293                InputStream is = mySocket.getInputStream();
294                if (is == null)
295                    return;
296
297                // Read the first 8192 bytes.
298                // The full header should fit in here.
299                // Apache's default header limit is 8KB.
300                // Do NOT assume that a single read will get the entire header at once!
301                final int bufsize = 8192;
302                byte[] buf = new byte[bufsize];
303                int splitbyte = 0;
304                int rlen = 0;
305                {
306                    int read = is.read(buf, 0, bufsize);
307                    while (read > 0) {
308                        rlen += read;
309                        splitbyte = findHeaderEnd(buf, rlen);
310                        if (splitbyte > 0)
311                            break;
312                        read = is.read(buf, rlen, bufsize - rlen);
313                    }
314                }
315
316                // Create a BufferedReader for parsing the header.
317                ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
318                BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
319                Properties pre = new Properties();
320                Map<String, String> parms = new HashMap<String, String>();
321                Map<String, String> header = new HashMap<String, String>();
322                Map<String, String> files = new HashMap<String, String>();
323
324                // Decode the header into parms and header java properties
325                decodeHeader(hin, pre, parms, header);
326                String method = pre.getProperty("method");
327                String uri = pre.getProperty("uri");
328
329                long size = 0x7FFFFFFFFFFFFFFFl;
330                String contentLength = header.get("content-length");
331                if (contentLength != null) {
332                    try {
333                        size = Integer.parseInt(contentLength);
334                    } catch (NumberFormatException ex) {
335                    }
336                }
337
338                // Write the part of body already read to ByteArrayOutputStream f
339                ByteArrayOutputStream f = new ByteArrayOutputStream();
340                if (splitbyte < rlen)
341                    f.write(buf, splitbyte, rlen - splitbyte);
342
343                // While Firefox sends on the first read all the data fitting
344                // our buffer, Chrome and Opera send only the headers even if
345                // there is data for the body. We do some magic here to find
346                // out whether we have already consumed part of body, if we
347                // have reached the end of the data to be sent or we should
348                // expect the first byte of the body at the next read.
349                if (splitbyte < rlen)
350                    size -= rlen - splitbyte + 1;
351                else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl)
352                    size = 0;
353
354                // Now read all the body and write it to f
355                buf = new byte[512];
356                while (rlen >= 0 && size > 0) {
357                    rlen = is.read(buf, 0, 512);
358                    size -= rlen;
359                    if (rlen > 0)
360                        f.write(buf, 0, rlen);
361                }
362
363                // Get the raw body as a byte []
364                byte[] fbuf = f.toByteArray();
365
366                // Create a BufferedReader for easily reading it as string.
367                ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
368                BufferedReader in = new BufferedReader(new InputStreamReader(bin));
369
370                // If the method is POST, there may be parameters
371                // in data section, too, read it:
372                if (method.equalsIgnoreCase("POST")) {
373                    String contentType = "";
374                    String contentTypeHeader = header.get("content-type");
375                    StringTokenizer st = new StringTokenizer(contentTypeHeader, "; ");
376                    if (st.hasMoreTokens()) {
377                        contentType = st.nextToken();
378                    }
379
380                    if (contentType.equalsIgnoreCase("multipart/form-data")) {
381                        // Handle multipart/form-data
382                        if (!st.hasMoreTokens())
383                            sendError(HTTP_BADREQUEST,
384                                    "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
385                        String boundaryExp = st.nextToken();
386                        st = new StringTokenizer(boundaryExp, "=");
387                        if (st.countTokens() != 2)
388                            sendError(HTTP_BADREQUEST,
389                                    "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html");
390                        st.nextToken();
391                        String boundary = st.nextToken();
392
393                        decodeMultipartData(boundary, fbuf, in, parms, files);
394                    } else {
395                        // Handle application/x-www-form-urlencoded
396                        String postLine = "";
397                        char pbuf[] = new char[512];
398                        int read = in.read(pbuf);
399                        while (read >= 0 && !postLine.endsWith("\r\n")) {
400                            postLine += String.valueOf(pbuf, 0, read);
401                            read = in.read(pbuf);
402                        }
403                        postLine = postLine.trim();
404                        decodeParms(postLine, parms);
405                    }
406                }
407
408                if (method.equalsIgnoreCase("PUT"))
409                    files.put("content", saveTmpFile(fbuf, 0, f.size()));
410
411                // Ok, now do the serve()
412                Response r = serve(uri, method, header, parms, files);
413                if (r == null)
414                    sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
415                else
416                    sendResponse(r.status, r.mimeType, r.header, r.data);
417
418                in.close();
419                is.close();
420            } catch (IOException ioe) {
421                try {
422                    sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
423                } catch (Throwable t) {
424                }
425            } catch (InterruptedException ie) {
426                // Thrown by sendError, ignore and exit the thread.
427            }
428        }
429
430        /**
431         * Decodes the sent headers and loads the data into java Properties' key - value pairs
432         **/
433        private void decodeHeader(BufferedReader in, Properties pre, Map<String, String> parms, Map<String, String> header)
434                throws InterruptedException {
435            try {
436                // Read the request line
437                String inLine = in.readLine();
438                if (inLine == null)
439                    return;
440                StringTokenizer st = new StringTokenizer(inLine);
441                if (!st.hasMoreTokens())
442                    sendError(HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
443
444                String method = st.nextToken();
445                pre.put("method", method);
446
447                if (!st.hasMoreTokens())
448                    sendError(HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
449
450                String uri = st.nextToken();
451
452                // Decode parameters from the URI
453                int qmi = uri.indexOf('?');
454                if (qmi >= 0) {
455                    decodeParms(uri.substring(qmi + 1), parms);
456                    uri = decodePercent(uri.substring(0, qmi));
457                } else
458                    uri = decodePercent(uri);
459
460                // If there's another token, it's protocol version,
461                // followed by HTTP headers. Ignore version but parse headers.
462                // NOTE: this now forces header names lowercase since they are
463                // case insensitive and vary by client.
464                if (st.hasMoreTokens()) {
465                    String line = in.readLine();
466                    while (line != null && line.trim().length() > 0) {
467                        int p = line.indexOf(':');
468                        if (p >= 0)
469                            header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
470                        line = in.readLine();
471                    }
472                }
473
474                pre.put("uri", uri);
475            } catch (IOException ioe) {
476                sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
477            }
478        }
479
480        /**
481         * Decodes the Multipart Body data and put it into java Properties' key - value pairs.
482         **/
483        private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Map<String, String> parms,
484                Map<String, String> files) throws InterruptedException {
485            try {
486                int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
487                int boundarycount = 1;
488                String mpline = in.readLine();
489                while (mpline != null) {
490                    if (mpline.indexOf(boundary) == -1)
491                        sendError(HTTP_BADREQUEST,
492                                "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
493                    boundarycount++;
494                    Map<String, String> item = new HashMap<String, String>();
495                    mpline = in.readLine();
496                    while (mpline != null && mpline.trim().length() > 0) {
497                        int p = mpline.indexOf(':');
498                        if (p != -1) {
499                            item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim());
500                        }
501                        mpline = in.readLine();
502                    }
503                    if (mpline != null) {
504                        String contentDisposition = item.get("content-disposition");
505                        if (contentDisposition == null) {
506                            sendError(HTTP_BADREQUEST,
507                                    "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
508                        }
509                        StringTokenizer st = new StringTokenizer(contentDisposition, "; ");
510                        Map<String, String> disposition = new HashMap<String, String>();
511                        while (st.hasMoreTokens()) {
512                            String token = st.nextToken();
513                            int p = token.indexOf('=');
514                            if (p != -1) {
515                                disposition.put(token.substring(0, p).trim().toLowerCase(), token.substring(p + 1).trim());
516                            }
517                        }
518                        String pname = disposition.get("name");
519                        pname = pname.substring(1, pname.length() - 1);
520
521                        String value = "";
522                        if (item.get("content-type") == null) {
523                            while (mpline != null && mpline.indexOf(boundary) == -1) {
524                                mpline = in.readLine();
525                                if (mpline != null) {
526                                    int d = mpline.indexOf(boundary);
527                                    if (d == -1) {
528                                        value += mpline;
529                                    } else {
530                                        value += mpline.substring(0, d - 2);
531                                    }
532                                }
533                            }
534                        } else {
535                            if (boundarycount > bpositions.length) {
536                                sendError(HTTP_INTERNALERROR, "Error processing request");
537                            }
538                            int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
539                            String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
540                            files.put(pname, path);
541                            value = disposition.get("filename");
542                            value = value.substring(1, value.length() - 1);
543                            do {
544                                mpline = in.readLine();
545                            } while (mpline != null && mpline.indexOf(boundary) == -1);
546                        }
547                        parms.put(pname, value);
548                    }
549                }
550            } catch (IOException ioe) {
551                sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
552            }
553        }
554
555        /**
556         * Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
557         **/
558        private int findHeaderEnd(final byte[] buf, int rlen) {
559            int splitbyte = 0;
560            while (splitbyte + 3 < rlen) {
561                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
562                    return splitbyte + 4;
563                }
564                splitbyte++;
565            }
566            return 0;
567        }
568
569        /**
570         * Find the byte positions where multipart boundaries start.
571         **/
572        public int[] getBoundaryPositions(byte[] b, byte[] boundary) {
573            int matchcount = 0;
574            int matchbyte = -1;
575            List<Integer> matchbytes = new ArrayList<Integer>();
576            for (int i = 0; i < b.length; i++) {
577                if (b[i] == boundary[matchcount]) {
578                    if (matchcount == 0)
579                        matchbyte = i;
580                    matchcount++;
581                    if (matchcount == boundary.length) {
582                        matchbytes.add(matchbyte);
583                        matchcount = 0;
584                        matchbyte = -1;
585                    }
586                } else {
587                    i -= matchcount;
588                    matchcount = 0;
589                    matchbyte = -1;
590                }
591            }
592            int[] ret = new int[matchbytes.size()];
593            for (int i = 0; i < ret.length; i++) {
594                ret[i] = (matchbytes.get(i)).intValue();
595            }
596            return ret;
597        }
598
599        /**
600         * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
601         **/
602        private String saveTmpFile(byte[] b, int offset, int len) {
603            String path = "";
604            if (len > 0) {
605                String tmpdir = System.getProperty("java.io.tmpdir");
606                try {
607                    File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
608                    OutputStream fstream = new FileOutputStream(temp);
609                    fstream.write(b, offset, len);
610                    fstream.close();
611                    path = temp.getAbsolutePath();
612                } catch (Exception e) { // Catch exception if any
613                    myErr.println("Error: " + e.getMessage());
614                }
615            }
616            return path;
617        }
618
619        /**
620         * It returns the offset separating multipart file headers from the file's data.
621         */
622        private int stripMultipartHeaders(byte[] b, int offset) {
623            int i;
624            for (i = offset; i < b.length; i++) {
625                if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n') {
626                    break;
627                }
628            }
629            return i + 1;
630        }
631
632        /**
633         * Decodes the percent encoding scheme. <br/>
634         * For example: "an+example%20string" -> "an example string"
635         */
636        private String decodePercent(String str) throws InterruptedException {
637            try {
638                StringBuffer sb = new StringBuffer();
639                for (int i = 0; i < str.length(); i++) {
640                    char c = str.charAt(i);
641                    switch (c) {
642                    case '+':
643                        sb.append(' ');
644                        break;
645                    case '%':
646                        sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16));
647                        i += 2;
648                        break;
649                    default:
650                        sb.append(c);
651                        break;
652                    }
653                }
654                return sb.toString();
655            } catch (Exception e) {
656                sendError(HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding.");
657                return null;
658            }
659        }
660
661        /**
662         * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
663         * Properties. NOTE: this doesn't support multiple identical keys due to the simplicity of Properties -- if you need multiples, you
664         * might want to replace the Properties with a Hashtable of Vectors or such.
665         */
666        private void decodeParms(String parms, Map<String, String> p) throws InterruptedException {
667            if (parms == null)
668                return;
669
670            StringTokenizer st = new StringTokenizer(parms, "&");
671            while (st.hasMoreTokens()) {
672                String e = st.nextToken();
673                int sep = e.indexOf('=');
674                if (sep >= 0) {
675                    p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
676                }
677            }
678        }
679
680        /**
681         * Returns an error message as a HTTP response and throws InterruptedException to stop further request processing.
682         */
683        private void sendError(String status, String msg) throws InterruptedException {
684            sendResponse(status, MIME_PLAINTEXT, null, new ByteArrayInputStream(msg.getBytes()));
685            throw new InterruptedException();
686        }
687
688        /**
689         * Sends given response to the socket.
690         */
691        private void sendResponse(String status, String mime, Map<String, String> header, InputStream data) {
692            try {
693                if (status == null) {
694                    throw new Error("sendResponse(): Status can't be null.");
695                }
696                OutputStream out = mySocket.getOutputStream();
697                PrintWriter pw = new PrintWriter(out);
698                pw.print("HTTP/1.0 " + status + " \r\n");
699
700                if (mime != null) {
701                    pw.print("Content-Type: " + mime + "\r\n");
702                }
703
704                if (header == null || header.get("Date") == null) {
705                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
706                }
707
708                if (header != null) {
709                    Iterator<String> e = header.keySet().iterator();
710                    while (e.hasNext()) {
711                        String key = e.next();
712                        String value = header.get(key);
713                        pw.print(key + ": " + value + "\r\n");
714                    }
715                }
716
717                pw.print("\r\n");
718                pw.flush();
719
720                if (data != null) {
721                    int pending = data.available(); // This is to support partial sends, see serveFile()
722                    byte[] buff = new byte[BUFFER_SIZE];
723                    while (pending > 0) {
724                        int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
725                        if (read <= 0) {
726                            break;
727                        }
728                        out.write(buff, 0, read);
729                        pending -= read;
730                    }
731                }
732                out.flush();
733                out.close();
734                if (data != null)
735                    data.close();
736            } catch (IOException ioe) {
737                // Couldn't write? No can do.
738                try {
739                    mySocket.close();
740                } catch (Throwable t) {
741                }
742            }
743        }
744
745        private final Socket mySocket;
746    }
747
748    /**
749     * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'.
750     */
751    private String encodeUri(String uri) {
752        String newUri = "";
753        StringTokenizer st = new StringTokenizer(uri, "/ ", true);
754        while (st.hasMoreTokens()) {
755            String tok = st.nextToken();
756            if (tok.equals("/"))
757                newUri += "/";
758            else if (tok.equals(" "))
759                newUri += "%20";
760            else {
761                newUri += URLEncoder.encode(tok);
762                // For Java 1.4 you'll want to use this instead:
763                // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
764            }
765        }
766        return newUri;
767    }
768
769    private final int myTcpPort;
770    private final ServerSocket myServerSocket;
771    private final Thread myThread;
772    private final File myRootDir;
773
774    // ==================================================
775    // File server code
776    // ==================================================
777
778    /**
779     * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters.
780     */
781    public Response serveFile(String uri, Map<String, String> header, File homeDir, boolean allowDirectoryListing) {
782        Response res = null;
783
784        // Make sure we won't die of an exception later
785        if (!homeDir.isDirectory())
786            res = new Response(HTTP_INTERNALERROR, MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory.");
787
788        if (res == null) {
789            // Remove URL arguments
790            uri = uri.trim().replace(File.separatorChar, '/');
791            if (uri.indexOf('?') >= 0)
792                uri = uri.substring(0, uri.indexOf('?'));
793
794            // Prohibit getting out of current directory
795            if (uri.startsWith("..") || uri.endsWith("..") || uri.indexOf("../") >= 0)
796                res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons.");
797        }
798
799        File f = new File(homeDir, uri);
800        if (res == null && !f.exists())
801            res = new Response(HTTP_NOTFOUND, MIME_PLAINTEXT, "Error 404, file not found.");
802
803        // List the directory, if necessary
804        if (res == null && f.isDirectory()) {
805            // Browsers get confused without '/' after the
806            // directory, send a redirect.
807            if (!uri.endsWith("/")) {
808                uri += "/";
809                res = new Response(HTTP_REDIRECT, MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri
810                        + "</a></body></html>");
811                res.addHeader("Location", uri);
812            }
813
814            if (res == null) {
815                // First try index.html and index.htm
816                if (new File(f, "index.html").exists())
817                    f = new File(homeDir, uri + "/index.html");
818                else if (new File(f, "index.htm").exists())
819                    f = new File(homeDir, uri + "/index.htm");
820                // No index file, list the directory if it is readable
821                else if (allowDirectoryListing && f.canRead()) {
822                    String[] files = f.list();
823                    String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
824
825                    if (uri.length() > 1) {
826                        String u = uri.substring(0, uri.length() - 1);
827                        int slash = u.lastIndexOf('/');
828                        if (slash >= 0 && slash < u.length())
829                            msg += "<b><a href=\"" + uri.substring(0, slash + 1) + "\">..</a></b><br/>";
830                    }
831
832                    if (files != null) {
833                        for (int i = 0; i < files.length; ++i) {
834                            File curFile = new File(f, files[i]);
835                            boolean dir = curFile.isDirectory();
836                            if (dir) {
837                                msg += "<b>";
838                                files[i] += "/";
839                            }
840
841                            msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">" + files[i] + "</a>";
842
843                            // Show file size
844                            if (curFile.isFile()) {
845                                long len = curFile.length();
846                                msg += " &nbsp;<font size=2>(";
847                                if (len < 1024)
848                                    msg += len + " bytes";
849                                else if (len < 1024 * 1024)
850                                    msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB";
851                                else
852                                    msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB";
853
854                                msg += ")</font>";
855                            }
856                            msg += "<br/>";
857                            if (dir)
858                                msg += "</b>";
859                        }
860                    }
861                    msg += "</body></html>";
862                    res = new Response(HTTP_OK, MIME_HTML, msg);
863                } else {
864                    res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: No directory listing.");
865                }
866            }
867        }
868
869        try {
870            if (res == null) {
871                // Get MIME type from file name extension, if possible
872                String mime = null;
873                int dot = f.getCanonicalPath().lastIndexOf('.');
874                if (dot >= 0)
875                    mime = MIME_TYPES.get(f.getCanonicalPath().substring(dot + 1).toLowerCase());
876                if (mime == null)
877                    mime = MIME_DEFAULT_BINARY;
878
879                // Calculate etag
880                String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
881
882                // Support (simple) skipping:
883                long startFrom = 0;
884                long endAt = -1;
885                String range = header.get("range");
886                if (range != null) {
887                    if (range.startsWith("bytes=")) {
888                        range = range.substring("bytes=".length());
889                        int minus = range.indexOf('-');
890                        try {
891                            if (minus > 0) {
892                                startFrom = Long.parseLong(range.substring(0, minus));
893                                endAt = Long.parseLong(range.substring(minus + 1));
894                            }
895                        } catch (NumberFormatException nfe) {
896                        }
897                    }
898                }
899
900                // Change return code and add Content-Range header when skipping is requested
901                long fileLen = f.length();
902                if (range != null && startFrom >= 0) {
903                    if (startFrom >= fileLen) {
904                        res = new Response(HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "");
905                        res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
906                        res.addHeader("ETag", etag);
907                    } else {
908                        if (endAt < 0)
909                            endAt = fileLen - 1;
910                        long newLen = endAt - startFrom + 1;
911                        if (newLen < 0)
912                            newLen = 0;
913
914                        final long dataLen = newLen;
915                        FileInputStream fis = new FileInputStream(f) {
916                            @Override
917                            public int available() throws IOException {
918                                return (int) dataLen;
919                            }
920                        };
921                        fis.skip(startFrom);
922
923                        res = new Response(HTTP_PARTIALCONTENT, mime, fis);
924                        res.addHeader("Content-Length", "" + dataLen);
925                        res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
926                        res.addHeader("ETag", etag);
927                    }
928                } else {
929                    if (etag.equals(header.get("if-none-match")))
930                        res = new Response(HTTP_NOTMODIFIED, mime, "");
931                    else {
932                        res = new Response(HTTP_OK, mime, new FileInputStream(f));
933                        res.addHeader("Content-Length", "" + fileLen);
934                        res.addHeader("ETag", etag);
935                    }
936                }
937            }
938        } catch (IOException ioe) {
939            res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed.");
940        }
941
942        res.addHeader("Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
943        return res;
944    }
945
946    /**
947     * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
948     */
949    private static final Map<String, String> MIME_TYPES;
950    static {
951        Map<String, String> mime = new HashMap<String, String>();
952        mime.put("css", "text/css");
953        mime.put("htm", "text/html");
954        mime.put("html", "text/html");
955        mime.put("xml", "text/xml");
956        mime.put("txt", "text/plain");
957        mime.put("asc", "text/plain");
958        mime.put("gif", "image/gif");
959        mime.put("jpg", "image/jpeg");
960        mime.put("jpeg", "image/jpeg");
961        mime.put("png", "image/png");
962        mime.put("mp3", "audio/mpeg");
963        mime.put("m3u", "audio/mpeg-url");
964        mime.put("mp4", "video/mp4");
965        mime.put("ogv", "video/ogg");
966        mime.put("flv", "video/x-flv");
967        mime.put("mov", "video/quicktime");
968        mime.put("swf", "application/x-shockwave-flash");
969        mime.put("js", "application/javascript");
970        mime.put("pdf", "application/pdf");
971        mime.put("doc", "application/msword");
972        mime.put("ogg", "application/x-ogg");
973        mime.put("zip", "application/octet-stream");
974        mime.put("exe", "application/octet-stream");
975        mime.put("class", "application/octet-stream");
976        MIME_TYPES = mime;
977    }
978
979    private static int BUFFER_SIZE = 16 * 1024;
980
981    // Change these if you want to log to somewhere else than stdout
982    protected static PrintStream myOut = System.out;
983    protected static PrintStream myErr = System.err;
984
985    /**
986     * GMT date formatter
987     */
988    private static java.text.SimpleDateFormat gmtFrmt;
989    static {
990        gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
991        gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
992    }
993
994    /**
995     * The distribution licence
996     */
997    private static final String LICENCE = "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"
998            + "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n" + "\n"
999            + "Redistribution and use in source and binary forms, with or without\n"
1000            + "modification, are permitted provided that the following conditions\n" + "are met:\n" + "\n"
1001            + "Redistributions of source code must retain the above copyright notice,\n"
1002            + "this list of conditions and the following disclaimer. Redistributions in\n"
1003            + "binary form must reproduce the above copyright notice, this list of\n"
1004            + "conditions and the following disclaimer in the documentation and/or other\n"
1005            + "materials provided with the distribution. The name of the author may not\n"
1006            + "be used to endorse or promote products derived from this software without\n" + "specific prior written permission. \n"
1007            + " \n" + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
1008            + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"
1009            + "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"
1010            + "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"
1011            + "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"
1012            + "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"
1013            + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"
1014            + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"
1015            + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
1016            + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
1017}
1018