diff options
author | Paul Hawke <paul.hawke@gmail.com> | 2013-01-05 10:40:49 -0600 |
---|---|---|
committer | Paul Hawke <paul.hawke@gmail.com> | 2013-01-05 10:40:49 -0600 |
commit | d22b56d68169cbf5fcf9ddfb30b54fb5f155cc40 (patch) | |
tree | 14664275a0fcd377e73050fa33a8ee7cb03df81e | |
parent | b5e00c4f65d8730522dec810a9078edd853be964 (diff) | |
download | nanohttpd-d22b56d68169cbf5fcf9ddfb30b54fb5f155cc40.tar.gz |
Updates - runs nicely now.
-rw-r--r-- | HelloServer.java | 45 | ||||
-rw-r--r-- | src/main/java/fi/iki/elonen/HelloServer.java | 46 | ||||
-rw-r--r-- | src/main/java/fi/iki/elonen/NanoHTTPD.java (renamed from fi/iki/elonen/NanoHTTPD.java) | 781 |
3 files changed, 424 insertions, 448 deletions
diff --git a/HelloServer.java b/HelloServer.java deleted file mode 100644 index f930f29..0000000 --- a/HelloServer.java +++ /dev/null @@ -1,45 +0,0 @@ -import java.io.*; -import java.util.*; - -/** - * An example of subclassing NanoHTTPD to make a custom HTTP server. - */ -public class HelloServer extends NanoHTTPD -{ - public HelloServer() throws IOException - { - super(8080, new File(".")); - } - - public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) - { - System.out.println( method + " '" + uri + "' " ); - String msg = "<html><body><h1>Hello server</h1>\n"; - if ( parms.getProperty("username") == null ) - msg += - "<form action='?' method='get'>\n" + - " <p>Your name: <input type='text' name='username'></p>\n" + - "</form>\n"; - else - msg += "<p>Hello, " + parms.getProperty("username") + "!</p>"; - - msg += "</body></html>\n"; - return new NanoHTTPD.Response( HTTP_OK, MIME_HTML, msg ); - } - - - public static void main( String[] args ) - { - try - { - new HelloServer(); - } - catch( IOException ioe ) - { - System.err.println( "Couldn't start server:\n" + ioe ); - System.exit( -1 ); - } - System.out.println( "Listening on port 8080. Hit Enter to stop.\n" ); - try { System.in.read(); } catch( Throwable t ) {}; - } -} diff --git a/src/main/java/fi/iki/elonen/HelloServer.java b/src/main/java/fi/iki/elonen/HelloServer.java new file mode 100644 index 0000000..f2a9f47 --- /dev/null +++ b/src/main/java/fi/iki/elonen/HelloServer.java @@ -0,0 +1,46 @@ +package fi.iki.elonen; + +import java.io.*; +import java.util.*; + +/** + * An example of subclassing NanoHTTPD to make a custom HTTP server. + */ +public class HelloServer extends NanoHTTPD { + public HelloServer() throws IOException { + super(8080, new File(".")); + } + + @Override + public Response serve(String uri, String method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) { + System.out.println(method + " '" + uri + "' "); + + String msg = "<html><body><h1>Hello server</h1>\n"; + if (parms.get("username") == null) + msg += + "<form action='?' method='get'>\n" + + " <p>Your name: <input type='text' name='username'></p>\n" + + "</form>\n"; + else + msg += "<p>Hello, " + parms.get("username") + "!</p>"; + + msg += "</body></html>\n"; + + return new NanoHTTPD.Response(msg); + } + + public static void main(String[] args) { + try { + new HelloServer().start(); + } catch (IOException ioe) { + System.err.println("Couldn't start server:\n" + ioe); + System.exit(-1); + } + + System.out.println("Listening on port 8080. Hit Enter to stop.\n"); + try { + System.in.read(); + } catch (Throwable t) { + } + } +} diff --git a/fi/iki/elonen/NanoHTTPD.java b/src/main/java/fi/iki/elonen/NanoHTTPD.java index 4dce55e..957ea4b 100644 --- a/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/fi/iki/elonen/NanoHTTPD.java @@ -1,17 +1,6 @@ package fi.iki.elonen; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintStream; -import java.io.PrintWriter; +import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.net.URLEncoder; @@ -28,15 +17,15 @@ import java.util.TimeZone; /** * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java - * - * <p> + * <p/> + * <p/> * NanoHTTPD version 1.25, Copyright © 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) and Copyright © 2010 * Konstantinos Togias (info@ktogias.gr, http://ktogias.gr) - * - * <p> + * <p/> + * <p/> * <b>Features + limitations: </b> * <ul> - * + * <p/> * <li>Only one Java file</li> * <li>Java 1.1 compatible</li> * <li>Released as open source, Modified BSD licence</li> @@ -57,125 +46,66 @@ import java.util.TimeZone; * <li>File server serves also very long files without memory overhead</li> * <li>Contains a built-in list of most common mime types</li> * <li>All header names are converted lowercase so they don't vary between browsers/clients</li> - * + * <p/> * </ul> - * - * <p> + * <p/> + * <p/> * <b>Ways to use: </b> * <ul> - * + * <p/> * <li>Run as a standalone app, serves files and shows requests</li> * <li>Subclass serve() and embed to your own program</li> * <li>Call serveFile() from serve() with your own base directory</li> - * + * <p/> * </ul> - * + * <p/> * See the end of the source file for distribution license (Modified BSD licence) */ -public class NanoHTTPD { - // ================================================== - // API parts - // ================================================== - +public abstract class NanoHTTPD { /** - * Override this to customize the server. - * <p> - * - * (By default, this delegates to serveFile() and allows directory listing.) - * - * @param uri - * Percent-decoded URI without parameters, for example "/index.cgi" - * @param method - * "GET", "POST" etc. - * @param parms - * Parsed, percent decoded parameters from URI and, in case of POST, data. - * @param header - * Header entries, percent decoded - * @return HTTP response, see class Response for details + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE */ - public Response serve(String uri, String method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) { - myOut.println(method + " '" + uri + "' "); - - Iterator<String> e = header.keySet().iterator(); - while (e.hasNext()) { - String value = e.next(); - myOut.println(" HDR: '" + value + "' = '" + header.get(value) + "'"); - } - e = parms.keySet().iterator(); - while (e.hasNext()) { - String value = e.next(); - myOut.println(" PRM: '" + value + "' = '" + parms.get(value) + "'"); - } - e = files.keySet().iterator(); - while (e.hasNext()) { - String value = e.next(); - myOut.println(" UPLOADED: '" + value + "' = '" + files.get(value) + "'"); - } + private static final Map<String, String> MIME_TYPES; - return serveFile(uri, header, myRootDir, true); + static { + Map<String, String> mime = new HashMap<String, String>(); + mime.put("css", "text/css"); + mime.put("htm", "text/html"); + mime.put("html", "text/html"); + mime.put("xml", "text/xml"); + mime.put("txt", "text/plain"); + mime.put("asc", "text/plain"); + mime.put("gif", "image/gif"); + mime.put("jpg", "image/jpeg"); + mime.put("jpeg", "image/jpeg"); + mime.put("png", "image/png"); + mime.put("mp3", "audio/mpeg"); + mime.put("m3u", "audio/mpeg-url"); + mime.put("mp4", "video/mp4"); + mime.put("ogv", "video/ogg"); + mime.put("flv", "video/x-flv"); + mime.put("mov", "video/quicktime"); + mime.put("swf", "application/x-shockwave-flash"); + mime.put("js", "application/javascript"); + mime.put("pdf", "application/pdf"); + mime.put("doc", "application/msword"); + mime.put("ogg", "application/x-ogg"); + mime.put("zip", "application/octet-stream"); + mime.put("exe", "application/octet-stream"); + mime.put("class", "application/octet-stream"); + MIME_TYPES = mime; } /** - * HTTP response. Return one of these from serve(). + * GMT date formatter */ - public class Response { - /** - * Default constructor: response = HTTP_OK, data = mime = 'null' - */ - public Response() { - this.status = HTTP_OK; - } - - /** - * Basic constructor. - */ - public Response(String status, String mimeType, InputStream data) { - this.status = status; - this.mimeType = mimeType; - this.data = data; - } - - /** - * Convenience method that makes an InputStream out of given text. - */ - public Response(String status, String mimeType, String txt) { - this.status = status; - this.mimeType = mimeType; - try { - this.data = new ByteArrayInputStream(txt.getBytes("UTF-8")); - } catch (java.io.UnsupportedEncodingException uee) { - uee.printStackTrace(); - } - } - - /** - * Adds given line to the header. - */ - public void addHeader(String name, String value) { - header.put(name, value); - } - - /** - * HTTP status code after processing, e.g. "200 OK", HTTP_OK - */ - public String status; - - /** - * MIME type of content, e.g. "text/html" - */ - public String mimeType; - - /** - * Data of the response, may be null. - */ - public InputStream data; + private static java.text.SimpleDateFormat gmtFrmt; - /** - * Headers for the HTTP response. Use addHeader() to add lines. - */ - public Map<String, String> header = new HashMap<String, String>(); + static { + gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); } - + /** * Some HTTP response status codes */ @@ -188,7 +118,6 @@ public class NanoHTTPD { public static final String HTTP_NOTFOUND = "404 Not Found"; public static final String HTTP_BADREQUEST = "400 Bad Request"; public static final String HTTP_INTERNALERROR = "500 Internal Server Error"; - public static final String HTTP_NOTIMPLEMENTED = "501 Not Implemented"; /** * Common mime types for dynamic content @@ -196,25 +125,23 @@ public class NanoHTTPD { public static final String MIME_PLAINTEXT = "text/plain"; public static final String MIME_HTML = "text/html"; public static final String MIME_DEFAULT_BINARY = "application/octet-stream"; - public static final String MIME_XML = "text/xml"; - // ================================================== - // Socket & server code - // ================================================== + private final ServerSocket myServerSocket; + private Thread myThread; + private final File myRootDir; /** - * Starts a HTTP server to given port. - * <p> - * Throws an IOException if the socket is already in use + * Constructs an HTTP server on given port. */ public NanoHTTPD(int port, File wwwroot) throws IOException { - myTcpPort = port; this.myRootDir = wwwroot; - myServerSocket = new ServerSocket(myTcpPort); + this.myServerSocket = new ServerSocket(port); } /** * Starts the server + * <p/> + * Throws an IOException if the socket is already in use */ public void start() { myThread = new Thread(new Runnable() { @@ -231,7 +158,7 @@ public class NanoHTTPD { myThread.setDaemon(true); myThread.start(); } - + /** * Stops the server. */ @@ -244,42 +171,212 @@ public class NanoHTTPD { } } + public File getRootDir() { + return myRootDir; + } + /** - * Starts as a standalone file server and waits for Enter. + * Override this to customize the server. + * <p/> + * <p/> + * (By default, this delegates to serveFile() and allows directory listing.) + * + * @param uri Percent-decoded URI without parameters, for example "/index.cgi" + * @param method "GET", "POST" etc. + * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. + * @param header Header entries, percent decoded + * @return HTTP response, see class Response for details */ - public static void main(String[] args) { - myOut.println("NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" - + "(Command line options: [-p port] [-d root-dir] [--licence])\n"); - - // Defaults - int port = 80; - File wwwroot = new File(".").getAbsoluteFile(); + public abstract Response serve(String uri, String method, Map<String, String> header, Map<String, String> parms, Map<String, String> files); - // Show licence if requested - for (int i = 0; i < args.length; ++i) - if (args[i].equalsIgnoreCase("-p")) - port = Integer.parseInt(args[i + 1]); - else if (args[i].equalsIgnoreCase("-d")) - wwwroot = new File(args[i + 1]).getAbsoluteFile(); - else if (args[i].toLowerCase().endsWith("licence")) { - myOut.println(LICENCE + "\n"); - break; + /** + * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'. + */ + private String encodeUri(String uri) { + String newUri = ""; + StringTokenizer st = new StringTokenizer(uri, "/ ", true); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equals("/")) + newUri += "/"; + else if (tok.equals(" ")) + newUri += "%20"; + else { + try { + newUri += URLEncoder.encode(tok, "UTF-8"); + } catch (UnsupportedEncodingException ignored) { + } } + } + return newUri; + } - try { - new NanoHTTPD(port, wwwroot); - } catch (IOException ioe) { - myErr.println("Couldn't start server:\n" + ioe); - System.exit(-1); + /** + * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters. + */ + public Response serveFile(String uri, Map<String, String> header, File homeDir, boolean allowDirectoryListing) { + Response res = null; + + // Make sure we won't die of an exception later + if (!homeDir.isDirectory()) + res = new Response(HTTP_INTERNALERROR, MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory."); + + if (res == null) { + // Remove URL arguments + uri = uri.trim().replace(File.separatorChar, '/'); + if (uri.indexOf('?') >= 0) + uri = uri.substring(0, uri.indexOf('?')); + + // Prohibit getting out of current directory + if (uri.startsWith("src/main") || uri.endsWith("src/main") || uri.indexOf("../") >= 0) + res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons."); } - myOut.println("Now serving files in port " + port + " from \"" + wwwroot + "\""); - myOut.println("Hit Enter to stop.\n"); + File f = new File(homeDir, uri); + if (res == null && !f.exists()) + res = new Response(HTTP_NOTFOUND, MIME_PLAINTEXT, "Error 404, file not found."); + + // List the directory, if necessary + if (res == null && f.isDirectory()) { + // Browsers get confused without '/' after the + // directory, send a redirect. + if (!uri.endsWith("/")) { + uri += "/"; + res = new Response(HTTP_REDIRECT, MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri + + "</a></body></html>"); + res.addHeader("Location", uri); + } + + if (res == null) { + // First try index.html and index.htm + if (new File(f, "index.html").exists()) + f = new File(homeDir, uri + "/index.html"); + else if (new File(f, "index.htm").exists()) + f = new File(homeDir, uri + "/index.htm"); + // No index file, list the directory if it is readable + else if (allowDirectoryListing && f.canRead()) { + String[] files = f.list(); + String msg = "<html><body><h1>Directory " + uri + "</h1><br/>"; + + if (uri.length() > 1) { + String u = uri.substring(0, uri.length() - 1); + int slash = u.lastIndexOf('/'); + if (slash >= 0 && slash < u.length()) + msg += "<b><a href=\"" + uri.substring(0, slash + 1) + "\">..</a></b><br/>"; + } + + if (files != null) { + for (int i = 0; i < files.length; ++i) { + File curFile = new File(f, files[i]); + boolean dir = curFile.isDirectory(); + if (dir) { + msg += "<b>"; + files[i] += "/"; + } + + msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">" + files[i] + "</a>"; + + // Show file size + if (curFile.isFile()) { + long len = curFile.length(); + msg += " <font size=2>("; + if (len < 1024) + msg += len + " bytes"; + else if (len < 1024 * 1024) + msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB"; + else + msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB"; + + msg += ")</font>"; + } + msg += "<br/>"; + if (dir) + msg += "</b>"; + } + } + msg += "</body></html>"; + res = new Response(HTTP_OK, MIME_HTML, msg); + } else { + res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: No directory listing."); + } + } + } try { - System.in.read(); - } catch (Throwable t) { + if (res == null) { + // Get MIME type from file name extension, if possible + String mime = null; + int dot = f.getCanonicalPath().lastIndexOf('.'); + if (dot >= 0) + mime = MIME_TYPES.get(f.getCanonicalPath().substring(dot + 1).toLowerCase()); + if (mime == null) + mime = MIME_DEFAULT_BINARY; + + // Calculate etag + String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode()); + + // Support (simple) skipping: + long startFrom = 0; + long endAt = -1; + String range = header.get("range"); + if (range != null) { + if (range.startsWith("bytes=")) { + range = range.substring("bytes=".length()); + int minus = range.indexOf('-'); + try { + if (minus > 0) { + startFrom = Long.parseLong(range.substring(0, minus)); + endAt = Long.parseLong(range.substring(minus + 1)); + } + } catch (NumberFormatException nfe) { + } + } + } + + // Change return code and add Content-Range header when skipping is requested + long fileLen = f.length(); + if (range != null && startFrom >= 0) { + if (startFrom >= fileLen) { + res = new Response(HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, ""); + res.addHeader("Content-Range", "bytes 0-0/" + fileLen); + res.addHeader("ETag", etag); + } else { + if (endAt < 0) + endAt = fileLen - 1; + long newLen = endAt - startFrom + 1; + if (newLen < 0) + newLen = 0; + + final long dataLen = newLen; + FileInputStream fis = new FileInputStream(f) { + @Override + public int available() throws IOException { + return (int) dataLen; + } + }; + fis.skip(startFrom); + + res = new Response(HTTP_PARTIALCONTENT, mime, fis); + res.addHeader("Content-Length", "" + dataLen); + res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); + res.addHeader("ETag", etag); + } + } else { + if (etag.equals(header.get("if-none-match"))) + res = new Response(HTTP_NOTMODIFIED, mime, ""); + else { + res = new Response(HTTP_OK, mime, new FileInputStream(f)); + res.addHeader("Content-Length", "" + fileLen); + res.addHeader("ETag", etag); + } + } + } + } catch (IOException ioe) { + res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed."); } + + res.addHeader("Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes + return res; } /** @@ -435,7 +532,7 @@ public class NanoHTTPD { /** * Decodes the sent headers and loads the data into java Properties' key - value pairs - **/ + */ private void decodeHeader(BufferedReader in, Properties pre, Map<String, String> parms, Map<String, String> header) throws InterruptedException { try { @@ -485,9 +582,9 @@ public class NanoHTTPD { /** * Decodes the Multipart Body data and put it into java Properties' key - value pairs. - **/ + */ private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Map<String, String> parms, - Map<String, String> files) throws InterruptedException { + Map<String, String> files) throws InterruptedException { try { int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); int boundarycount = 1; @@ -560,7 +657,7 @@ public class NanoHTTPD { /** * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. - **/ + */ private int findHeaderEnd(final byte[] buf, int rlen) { int splitbyte = 0; while (splitbyte + 3 < rlen) { @@ -574,7 +671,7 @@ public class NanoHTTPD { /** * Find the byte positions where multipart boundaries start. - **/ + */ public int[] getBoundaryPositions(byte[] b, byte[] boundary) { int matchcount = 0; int matchbyte = -1; @@ -604,7 +701,7 @@ public class NanoHTTPD { /** * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. - **/ + */ private String saveTmpFile(byte[] b, int offset, int len) { String path = ""; if (len > 0) { @@ -616,7 +713,7 @@ public class NanoHTTPD { fstream.close(); path = temp.getAbsolutePath(); } catch (Exception e) { // Catch exception if any - myErr.println("Error: " + e.getMessage()); + System.err.println("Error: " + e.getMessage()); } } return path; @@ -645,16 +742,16 @@ public class NanoHTTPD { for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); switch (c) { - case '+': - sb.append(' '); - break; - case '%': - sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16)); - i += 2; - break; - default: - sb.append(c); - break; + case '+': + sb.append(' '); + break; + case '%': + sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16)); + i += 2; + break; + default: + sb.append(c); + break; } } return sb.toString(); @@ -725,6 +822,7 @@ public class NanoHTTPD { if (data != null) { int pending = data.available(); // This is to support partial sends, see serveFile() + int BUFFER_SIZE = 16 * 1024; byte[] buff = new byte[BUFFER_SIZE]; while (pending > 0) { int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); @@ -752,249 +850,64 @@ public class NanoHTTPD { } /** - * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'. + * HTTP response. Return one of these from serve(). */ - private String encodeUri(String uri) { - String newUri = ""; - StringTokenizer st = new StringTokenizer(uri, "/ ", true); - while (st.hasMoreTokens()) { - String tok = st.nextToken(); - if (tok.equals("/")) - newUri += "/"; - else if (tok.equals(" ")) - newUri += "%20"; - else { - newUri += URLEncoder.encode(tok); - // For Java 1.4 you'll want to use this instead: - // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {} - } + public class Response { + /** + * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message + */ + public Response(String msg) { + this(HTTP_OK, MIME_HTML, msg); } - return newUri; - } - - private final int myTcpPort; - private final ServerSocket myServerSocket; - private final Thread myThread; - private final File myRootDir; - - // ================================================== - // File server code - // ================================================== - - /** - * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters. - */ - public Response serveFile(String uri, Map<String, String> header, File homeDir, boolean allowDirectoryListing) { - Response res = null; - - // Make sure we won't die of an exception later - if (!homeDir.isDirectory()) - res = new Response(HTTP_INTERNALERROR, MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory."); - if (res == null) { - // Remove URL arguments - uri = uri.trim().replace(File.separatorChar, '/'); - if (uri.indexOf('?') >= 0) - uri = uri.substring(0, uri.indexOf('?')); - - // Prohibit getting out of current directory - if (uri.startsWith("..") || uri.endsWith("..") || uri.indexOf("../") >= 0) - res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons."); + /** + * Basic constructor. + */ + public Response(String status, String mimeType, InputStream data) { + this.status = status; + this.mimeType = mimeType; + this.data = data; } - File f = new File(homeDir, uri); - if (res == null && !f.exists()) - res = new Response(HTTP_NOTFOUND, MIME_PLAINTEXT, "Error 404, file not found."); - - // List the directory, if necessary - if (res == null && f.isDirectory()) { - // Browsers get confused without '/' after the - // directory, send a redirect. - if (!uri.endsWith("/")) { - uri += "/"; - res = new Response(HTTP_REDIRECT, MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri - + "</a></body></html>"); - res.addHeader("Location", uri); - } - - if (res == null) { - // First try index.html and index.htm - if (new File(f, "index.html").exists()) - f = new File(homeDir, uri + "/index.html"); - else if (new File(f, "index.htm").exists()) - f = new File(homeDir, uri + "/index.htm"); - // No index file, list the directory if it is readable - else if (allowDirectoryListing && f.canRead()) { - String[] files = f.list(); - String msg = "<html><body><h1>Directory " + uri + "</h1><br/>"; - - if (uri.length() > 1) { - String u = uri.substring(0, uri.length() - 1); - int slash = u.lastIndexOf('/'); - if (slash >= 0 && slash < u.length()) - msg += "<b><a href=\"" + uri.substring(0, slash + 1) + "\">..</a></b><br/>"; - } - - if (files != null) { - for (int i = 0; i < files.length; ++i) { - File curFile = new File(f, files[i]); - boolean dir = curFile.isDirectory(); - if (dir) { - msg += "<b>"; - files[i] += "/"; - } - - msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">" + files[i] + "</a>"; - - // Show file size - if (curFile.isFile()) { - long len = curFile.length(); - msg += " <font size=2>("; - if (len < 1024) - msg += len + " bytes"; - else if (len < 1024 * 1024) - msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB"; - else - msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB"; - - msg += ")</font>"; - } - msg += "<br/>"; - if (dir) - msg += "</b>"; - } - } - msg += "</body></html>"; - res = new Response(HTTP_OK, MIME_HTML, msg); - } else { - res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: No directory listing."); - } + /** + * Convenience method that makes an InputStream out of given text. + */ + public Response(String status, String mimeType, String txt) { + this.status = status; + this.mimeType = mimeType; + try { + this.data = new ByteArrayInputStream(txt.getBytes("UTF-8")); + } catch (java.io.UnsupportedEncodingException uee) { + uee.printStackTrace(); } } - try { - if (res == null) { - // Get MIME type from file name extension, if possible - String mime = null; - int dot = f.getCanonicalPath().lastIndexOf('.'); - if (dot >= 0) - mime = MIME_TYPES.get(f.getCanonicalPath().substring(dot + 1).toLowerCase()); - if (mime == null) - mime = MIME_DEFAULT_BINARY; - - // Calculate etag - String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode()); - - // Support (simple) skipping: - long startFrom = 0; - long endAt = -1; - String range = header.get("range"); - if (range != null) { - if (range.startsWith("bytes=")) { - range = range.substring("bytes=".length()); - int minus = range.indexOf('-'); - try { - if (minus > 0) { - startFrom = Long.parseLong(range.substring(0, minus)); - endAt = Long.parseLong(range.substring(minus + 1)); - } - } catch (NumberFormatException nfe) { - } - } - } - - // Change return code and add Content-Range header when skipping is requested - long fileLen = f.length(); - if (range != null && startFrom >= 0) { - if (startFrom >= fileLen) { - res = new Response(HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, ""); - res.addHeader("Content-Range", "bytes 0-0/" + fileLen); - res.addHeader("ETag", etag); - } else { - if (endAt < 0) - endAt = fileLen - 1; - long newLen = endAt - startFrom + 1; - if (newLen < 0) - newLen = 0; - - final long dataLen = newLen; - FileInputStream fis = new FileInputStream(f) { - @Override - public int available() throws IOException { - return (int) dataLen; - } - }; - fis.skip(startFrom); - - res = new Response(HTTP_PARTIALCONTENT, mime, fis); - res.addHeader("Content-Length", "" + dataLen); - res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); - res.addHeader("ETag", etag); - } - } else { - if (etag.equals(header.get("if-none-match"))) - res = new Response(HTTP_NOTMODIFIED, mime, ""); - else { - res = new Response(HTTP_OK, mime, new FileInputStream(f)); - res.addHeader("Content-Length", "" + fileLen); - res.addHeader("ETag", etag); - } - } - } - } catch (IOException ioe) { - res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed."); + /** + * Adds given line to the header. + */ + public void addHeader(String name, String value) { + header.put(name, value); } - res.addHeader("Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes - return res; - } - - /** - * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE - */ - private static final Map<String, String> MIME_TYPES; - static { - Map<String, String> mime = new HashMap<String, String>(); - mime.put("css", "text/css"); - mime.put("htm", "text/html"); - mime.put("html", "text/html"); - mime.put("xml", "text/xml"); - mime.put("txt", "text/plain"); - mime.put("asc", "text/plain"); - mime.put("gif", "image/gif"); - mime.put("jpg", "image/jpeg"); - mime.put("jpeg", "image/jpeg"); - mime.put("png", "image/png"); - mime.put("mp3", "audio/mpeg"); - mime.put("m3u", "audio/mpeg-url"); - mime.put("mp4", "video/mp4"); - mime.put("ogv", "video/ogg"); - mime.put("flv", "video/x-flv"); - mime.put("mov", "video/quicktime"); - mime.put("swf", "application/x-shockwave-flash"); - mime.put("js", "application/javascript"); - mime.put("pdf", "application/pdf"); - mime.put("doc", "application/msword"); - mime.put("ogg", "application/x-ogg"); - mime.put("zip", "application/octet-stream"); - mime.put("exe", "application/octet-stream"); - mime.put("class", "application/octet-stream"); - MIME_TYPES = mime; - } + /** + * HTTP status code after processing, e.g. "200 OK", HTTP_OK + */ + public String status; - private static int BUFFER_SIZE = 16 * 1024; + /** + * MIME type of content, e.g. "text/html" + */ + public String mimeType; - // Change these if you want to log to somewhere else than stdout - protected static PrintStream myOut = System.out; - protected static PrintStream myErr = System.err; + /** + * Data of the response, may be null. + */ + public InputStream data; - /** - * GMT date formatter - */ - private static java.text.SimpleDateFormat gmtFrmt; - static { - gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); - gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + /** + * Headers for the HTTP response. Use addHeader() to add lines. + */ + public Map<String, String> header = new HashMap<String, String>(); } /** @@ -1009,7 +922,8 @@ public class NanoHTTPD { + "binary form must reproduce the above copyright notice, this list of\n" + "conditions and the following disclaimer in the documentation and/or other\n" + "materials provided with the distribution. The name of the author may not\n" - + "be used to endorse or promote products derived from this software without\n" + "specific prior written permission. \n" + + "be used to endorse or promote products derived from this software without\n" + + "specific prior written permission. \n" + " \n" + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n" + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n" + "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n" @@ -1020,4 +934,65 @@ public class NanoHTTPD { + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."; + + /** + * Starts as a standalone file server and waits for Enter. + */ + public static void main(String[] args) { + System.out.println("NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" + + "(Command line options: [-p port] [-d root-dir] [--licence])\n"); + + // Defaults + int port = 8080; + File wwwroot = new File(".").getAbsoluteFile(); + + // Show licence if requested + for (int i = 0; i < args.length; ++i) + if (args[i].equalsIgnoreCase("-p")) + port = Integer.parseInt(args[i + 1]); + else if (args[i].equalsIgnoreCase("-d")) + wwwroot = new File(args[i + 1]).getAbsoluteFile(); + else if (args[i].toLowerCase().endsWith("licence")) { + System.out.println(LICENCE + "\n"); + break; + } + + try { + new NanoHTTPD(port, wwwroot) { + public Response serve(String uri, String method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) { + System.out.println(method + " '" + uri + "' "); + + Iterator<String> e = header.keySet().iterator(); + while (e.hasNext()) { + String value = e.next(); + System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'"); + } + e = parms.keySet().iterator(); + while (e.hasNext()) { + String value = e.next(); + System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'"); + } + e = files.keySet().iterator(); + while (e.hasNext()) { + String value = e.next(); + System.out.println(" UPLOADED: '" + value + "' = '" + files.get(value) + "'"); + } + + return serveFile(uri, header, getRootDir(), true); + } + }.start(); + } catch (IOException ioe) { + System.err.println("Couldn't start server:\n" + ioe); + System.exit(-1); + } + + System.out.println("Now serving files in port " + port + " from \"" + wwwroot + "\""); + System.out.println("Hit Enter to stop.\n"); + + try { + @SuppressWarnings("unused") + int a = System.in.read(); + } catch (Throwable ignored) { + } + } } |