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