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