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 += " <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