SimpleWebServer.java revision dd4a404e2d9a5a6caf72dba361f1039916b6a87c
1package fi.iki.elonen;
2
3import java.io.File;
4import java.io.FileInputStream;
5import java.io.IOException;
6import java.io.UnsupportedEncodingException;
7import java.net.URLEncoder;
8import java.util.HashMap;
9import java.util.Iterator;
10import java.util.Map;
11import java.util.StringTokenizer;
12
13public class SimpleWebServer extends NanoHTTPD {
14    /**
15     * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
16     */
17    private static final Map<String, String> MIME_TYPES = new HashMap<String, String>() {{
18        put("css", "text/css");
19        put("htm", "text/html");
20        put("html", "text/html");
21        put("xml", "text/xml");
22        put("txt", "text/plain");
23        put("asc", "text/plain");
24        put("gif", "image/gif");
25        put("jpg", "image/jpeg");
26        put("jpeg", "image/jpeg");
27        put("png", "image/png");
28        put("mp3", "audio/mpeg");
29        put("m3u", "audio/mpeg-url");
30        put("mp4", "video/mp4");
31        put("ogv", "video/ogg");
32        put("flv", "video/x-flv");
33        put("mov", "video/quicktime");
34        put("swf", "application/x-shockwave-flash");
35        put("js", "application/javascript");
36        put("pdf", "application/pdf");
37        put("doc", "application/msword");
38        put("ogg", "application/x-ogg");
39        put("zip", "application/octet-stream");
40        put("exe", "application/octet-stream");
41        put("class", "application/octet-stream");
42    }};
43
44    /**
45     * The distribution licence
46     */
47    private static final String LICENCE =
48            "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>,\n"
49                    + "(C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"
50                    + "and (C) 2012- by Paul S. Hawke\n"
51                    + "\n"
52                    + "Redistribution and use in source and binary forms, with or without\n"
53                    + "modification, are permitted provided that the following conditions\n"
54                    + "are met:\n"
55                    + "\n"
56                    + "Redistributions of source code must retain the above copyright notice,\n"
57                    + "this list of conditions and the following disclaimer. Redistributions in\n"
58                    + "binary form must reproduce the above copyright notice, this list of\n"
59                    + "conditions and the following disclaimer in the documentation and/or other\n"
60                    + "materials provided with the distribution. The name of the author may not\n"
61                    + "be used to endorse or promote products derived from this software without\n"
62                    + "specific prior written permission. \n"
63                    + " \n"
64                    + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
65                    + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"
66                    + "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"
67                    + "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"
68                    + "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"
69                    + "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"
70                    + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"
71                    + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"
72                    + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
73                    + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
74
75    private File rootDir;
76
77    public SimpleWebServer(String host, int port, File wwwroot) {
78        super(host, port);
79        this.rootDir = wwwroot;
80    }
81
82    public File getRootDir() {
83        return rootDir;
84    }
85
86    /**
87     * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'.
88     */
89    private String encodeUri(String uri) {
90        String newUri = "";
91        StringTokenizer st = new StringTokenizer(uri, "/ ", true);
92        while (st.hasMoreTokens()) {
93            String tok = st.nextToken();
94            if (tok.equals("/"))
95                newUri += "/";
96            else if (tok.equals(" "))
97                newUri += "%20";
98            else {
99                try {
100                    newUri += URLEncoder.encode(tok, "UTF-8");
101                } catch (UnsupportedEncodingException ignored) {
102                }
103            }
104        }
105        return newUri;
106    }
107
108    /**
109     * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters.
110     */
111    public Response serveFile(String uri, Map<String, String> header, File homeDir) {
112        Response res = null;
113
114        // Make sure we won't die of an exception later
115        if (!homeDir.isDirectory())
116            res = new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory.");
117
118        if (res == null) {
119            // Remove URL arguments
120            uri = uri.trim().replace(File.separatorChar, '/');
121            if (uri.indexOf('?') >= 0)
122                uri = uri.substring(0, uri.indexOf('?'));
123
124            // Prohibit getting out of current directory
125            if (uri.startsWith("src/main") || uri.endsWith("src/main") || uri.contains("../"))
126                res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons.");
127        }
128
129        File f = new File(homeDir, uri);
130        if (res == null && !f.exists())
131            res = new Response(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
132
133        // List the directory, if necessary
134        if (res == null && f.isDirectory()) {
135            // Browsers get confused without '/' after the
136            // directory, send a redirect.
137            if (!uri.endsWith("/")) {
138                uri += "/";
139                res = new Response(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri
140                        + "</a></body></html>");
141                res.addHeader("Location", uri);
142            }
143
144            if (res == null) {
145                // First try index.html and index.htm
146                if (new File(f, "index.html").exists())
147                    f = new File(homeDir, uri + "/index.html");
148                else if (new File(f, "index.htm").exists())
149                    f = new File(homeDir, uri + "/index.htm");
150                    // No index file, list the directory if it is readable
151                else if (f.canRead()) {
152                    String[] files = f.list();
153                    String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
154
155                    if (uri.length() > 1) {
156                        String u = uri.substring(0, uri.length() - 1);
157                        int slash = u.lastIndexOf('/');
158                        if (slash >= 0 && slash < u.length())
159                            msg += "<b><a href=\"" + uri.substring(0, slash + 1) + "\">..</a></b><br/>";
160                    }
161
162                    if (files != null) {
163                        for (int i = 0; i < files.length; ++i) {
164                            File curFile = new File(f, files[i]);
165                            boolean dir = curFile.isDirectory();
166                            if (dir) {
167                                msg += "<b>";
168                                files[i] += "/";
169                            }
170
171                            msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">" + files[i] + "</a>";
172
173                            // Show file size
174                            if (curFile.isFile()) {
175                                long len = curFile.length();
176                                msg += " &nbsp;<font size=2>(";
177                                if (len < 1024)
178                                    msg += len + " bytes";
179                                else if (len < 1024 * 1024)
180                                    msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB";
181                                else
182                                    msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB";
183
184                                msg += ")</font>";
185                            }
186                            msg += "<br/>";
187                            if (dir)
188                                msg += "</b>";
189                        }
190                    }
191                    msg += "</body></html>";
192                    res = new Response(msg);
193                } else {
194                    res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: No directory listing.");
195                }
196            }
197        }
198
199        try {
200            if (res == null) {
201                // Get MIME type from file name extension, if possible
202                String mime = null;
203                int dot = f.getCanonicalPath().lastIndexOf('.');
204                if (dot >= 0)
205                    mime = MIME_TYPES.get(f.getCanonicalPath().substring(dot + 1).toLowerCase());
206                if (mime == null)
207                    mime = NanoHTTPD.MIME_DEFAULT_BINARY;
208
209                // Calculate etag
210                String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
211
212                // Support (simple) skipping:
213                long startFrom = 0;
214                long endAt = -1;
215                String range = header.get("range");
216                if (range != null) {
217                    if (range.startsWith("bytes=")) {
218                        range = range.substring("bytes=".length());
219                        int minus = range.indexOf('-');
220                        try {
221                            if (minus > 0) {
222                                startFrom = Long.parseLong(range.substring(0, minus));
223                                endAt = Long.parseLong(range.substring(minus + 1));
224                            }
225                        } catch (NumberFormatException ignored) {
226                        }
227                    }
228                }
229
230                // Change return code and add Content-Range header when skipping is requested
231                long fileLen = f.length();
232                if (range != null && startFrom >= 0) {
233                    if (startFrom >= fileLen) {
234                        res = new Response(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
235                        res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
236                        res.addHeader("ETag", etag);
237                    } else {
238                        if (endAt < 0)
239                            endAt = fileLen - 1;
240                        long newLen = endAt - startFrom + 1;
241                        if (newLen < 0)
242                            newLen = 0;
243
244                        final long dataLen = newLen;
245                        FileInputStream fis = new FileInputStream(f) {
246                            @Override
247                            public int available() throws IOException {
248                                return (int) dataLen;
249                            }
250                        };
251                        fis.skip(startFrom);
252
253                        res = new Response(Response.Status.PARTIAL_CONTENT, mime, fis);
254                        res.addHeader("Content-Length", "" + dataLen);
255                        res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
256                        res.addHeader("ETag", etag);
257                    }
258                } else {
259                    if (etag.equals(header.get("if-none-match")))
260                        res = new Response(Response.Status.NOT_MODIFIED, mime, "");
261                    else {
262                        res = new Response(Response.Status.OK, mime, new FileInputStream(f));
263                        res.addHeader("Content-Length", "" + fileLen);
264                        res.addHeader("ETag", etag);
265                    }
266                }
267            }
268        } catch (IOException ioe) {
269            res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: Reading file failed.");
270        }
271
272        res.addHeader("Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
273        return res;
274    }
275
276    @Override
277    public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {
278        System.out.println(method + " '" + uri + "' ");
279
280        Iterator<String> e = header.keySet().iterator();
281        while (e.hasNext()) {
282            String value = e.next();
283            System.out.println("  HDR: '" + value + "' = '" + header.get(value) + "'");
284        }
285        e = parms.keySet().iterator();
286        while (e.hasNext()) {
287            String value = e.next();
288            System.out.println("  PRM: '" + value + "' = '" + parms.get(value) + "'");
289        }
290        e = files.keySet().iterator();
291        while (e.hasNext()) {
292            String value = e.next();
293            System.out.println("  UPLOADED: '" + value + "' = '" + files.get(value) + "'");
294        }
295
296        return serveFile(uri, header, getRootDir());
297    }
298
299    /**
300     * Starts as a standalone file server and waits for Enter.
301     */
302    public static void main(String[] args) {
303        System.out.println(
304                "NanoHttpd 2.0: Command line options: [-h hostname] [-p port] [-d root-dir] [--licence]\n" +
305                        "(C) 2001,2005-2011 Jarno Elonen \n" +
306                        "(C) 2010 Konstantinos Togias\n" +
307                        "(C) 2012- Paul S. Hawke\n");
308
309        // Defaults
310        int port = 8080;
311        String host = "127.0.0.1";
312        File wwwroot = new File(".").getAbsoluteFile();
313
314        // Show licence if requested
315        for (int i = 0; i < args.length; ++i)
316            if (args[i].equalsIgnoreCase("-h"))
317                host = args[i + 1];
318            else if (args[i].equalsIgnoreCase("-p"))
319                port = Integer.parseInt(args[i + 1]);
320            else if (args[i].equalsIgnoreCase("-d"))
321                wwwroot = new File(args[i + 1]).getAbsoluteFile();
322            else if (args[i].toLowerCase().endsWith("licence")) {
323                System.out.println(LICENCE + "\n");
324                break;
325            }
326
327        ServerRunner.executeInstance(new SimpleWebServer(host, port, wwwroot));
328    }
329}
330