diff options
Diffstat (limited to 'webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java')
-rw-r--r-- | webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java | 610 |
1 files changed, 336 insertions, 274 deletions
diff --git a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java index ed32dd7..3f176fe 100644 --- a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java +++ b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java @@ -1,7 +1,41 @@ package fi.iki.elonen; +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; @@ -17,103 +51,46 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.StringTokenizer; +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.util.ServerRunner; + public class SimpleWebServer extends NanoHTTPD { - /** - * Common mime type for dynamic content: binary - */ - public static final String MIME_DEFAULT_BINARY = "application/octet-stream"; + /** * Default Index file names. */ - public static final List<String> INDEX_FILE_NAMES = new ArrayList<String>() {{ - add("index.html"); - add("index.htm"); - }}; - /** - * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE - */ - private static final Map<String, String> MIME_TYPES = new HashMap<String, String>() {{ - put("css", "text/css"); - put("htm", "text/html"); - put("html", "text/html"); - put("xml", "text/xml"); - put("java", "text/x-java-source, text/java"); - put("md", "text/plain"); - put("txt", "text/plain"); - put("asc", "text/plain"); - put("gif", "image/gif"); - put("jpg", "image/jpeg"); - put("jpeg", "image/jpeg"); - put("png", "image/png"); - put("mp3", "audio/mpeg"); - put("m3u", "audio/mpeg-url"); - put("mp4", "video/mp4"); - put("ogv", "video/ogg"); - put("flv", "video/x-flv"); - put("mov", "video/quicktime"); - put("swf", "application/x-shockwave-flash"); - put("js", "application/javascript"); - put("pdf", "application/pdf"); - put("doc", "application/msword"); - put("ogg", "application/x-ogg"); - put("zip", "application/octet-stream"); - put("exe", "application/octet-stream"); - put("class", "application/octet-stream"); - }}; + @SuppressWarnings("serial") + public static final List<String> INDEX_FILE_NAMES = new ArrayList<String>() { + + { + add("index.html"); + add("index.htm"); + } + }; + /** * The distribution licence */ - private static final String LICENCE = - "Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias\n" - + "\n" - + "Redistribution and use in source and binary forms, with or without\n" - + "modification, are permitted provided that the following conditions\n" - + "are met:\n" - + "\n" - + "Redistributions of source code must retain the above copyright notice,\n" - + "this list of conditions and the following disclaimer. Redistributions in\n" - + "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" - + " \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" - + "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n" - + "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n" - + "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" - + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" - + "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."; - private static Map<String, WebServerPlugin> mimeTypeHandlers = new HashMap<String, WebServerPlugin>(); - private final List<File> rootDirs; - private final boolean quiet; - - public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) { - super(host, port); - this.quiet = quiet; - this.rootDirs = new ArrayList<File>(); - this.rootDirs.add(wwwroot); - - this.init(); - } - - public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) { - super(host, port); - this.quiet = quiet; - this.rootDirs = new ArrayList<File>(wwwroots); - - this.init(); + private static final String LICENCE; + static { + mimeTypes(); + InputStream stream = SimpleWebServer.class.getResourceAsStream("/LICENSE.txt"); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + String text; + try { + while ((count = stream.read(buffer)) >= 0) { + bytes.write(buffer, 0, count); + } + text = bytes.toString("UTF-8"); + } catch (IOException e) { + text = "unknown"; + } + LICENCE = text; } - /** - * Used to initialize and customize the server. - */ - public void init() { - } + private static Map<String, WebServerPlugin> mimeTypeHandlers = new HashMap<String, WebServerPlugin>(); /** * Starts as a standalone file server and waits for Enter. @@ -122,9 +99,10 @@ public class SimpleWebServer extends NanoHTTPD { // Defaults int port = 8080; - String host = "127.0.0.1"; + String host = null; // bind to all interfaces by default List<File> rootDirs = new ArrayList<File>(); boolean quiet = false; + String cors = null; Map<String, String> options = new HashMap<String, String>(); // Parse command-line, with short and long versions of the options. @@ -137,8 +115,14 @@ public class SimpleWebServer extends NanoHTTPD { quiet = true; } else if (args[i].equalsIgnoreCase("-d") || args[i].equalsIgnoreCase("--dir")) { rootDirs.add(new File(args[i + 1]).getAbsoluteFile()); + } else if (args[i].startsWith("--cors")) { + cors = "*"; + int equalIdx = args[i].indexOf('='); + if (equalIdx > 0) { + cors = args[i].substring(equalIdx + 1); + } } else if (args[i].equalsIgnoreCase("--licence")) { - System.out.println(LICENCE + "\n"); + System.out.println(SimpleWebServer.LICENCE + "\n"); } else if (args[i].startsWith("-X:")) { int dot = args[i].indexOf('='); if (dot > 0) { @@ -152,9 +136,8 @@ public class SimpleWebServer extends NanoHTTPD { if (rootDirs.isEmpty()) { rootDirs.add(new File(".").getAbsoluteFile()); } - options.put("host", host); - options.put("port", ""+port); + options.put("port", "" + port); options.put("quiet", String.valueOf(quiet)); StringBuilder sb = new StringBuilder(); for (File dir : rootDirs) { @@ -163,10 +146,10 @@ public class SimpleWebServer extends NanoHTTPD { } try { sb.append(dir.getCanonicalPath()); - } catch (IOException ignored) {} + } catch (IOException ignored) { + } } options.put("home", sb.toString()); - ServiceLoader<WebServerPluginInfo> serviceLoader = ServiceLoader.load(WebServerPluginInfo.class); for (WebServerPluginInfo info : serviceLoader) { String[] mimeTypes = info.getMimeTypes(); @@ -185,8 +168,7 @@ public class SimpleWebServer extends NanoHTTPD { registerPluginForMimeType(indexFiles, mime, info.getWebServerPlugin(mime), options); } } - - ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet)); + ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet, cors)); } protected static void registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map<String, String> commandLineOptions) { @@ -199,40 +181,69 @@ public class SimpleWebServer extends NanoHTTPD { int dot = filename.lastIndexOf('.'); if (dot >= 0) { String extension = filename.substring(dot + 1).toLowerCase(); - MIME_TYPES.put(extension, mimeType); + mimeTypes().put(extension, mimeType); } } - INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles)); + SimpleWebServer.INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles)); } - mimeTypeHandlers.put(mimeType, plugin); + SimpleWebServer.mimeTypeHandlers.put(mimeType, plugin); plugin.initialize(commandLineOptions); } - private File getRootDir() { - return rootDirs.get(0); + private final boolean quiet; + + private final String cors; + + protected List<File> rootDirs; + + public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) { + this(host, port, Collections.singletonList(wwwroot), quiet, cors); + } + + public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) { + this(host, port, Collections.singletonList(wwwroot), quiet, null); } - private List<File> getRootDirs() { - return rootDirs; + public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) { + this(host, port, wwwroots, quiet, null); } - private void addWwwRootDir(File wwwroot) { - rootDirs.add(wwwroot); + public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet, String cors) { + super(host, port); + this.quiet = quiet; + this.cors = cors; + this.rootDirs = new ArrayList<File>(wwwroots); + + init(); + } + + private boolean canServeUri(String uri, File homeDir) { + boolean canServeUri; + File f = new File(homeDir, uri); + canServeUri = f.exists(); + if (!canServeUri) { + WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(getMimeTypeForFile(uri)); + if (plugin != null) { + canServeUri = plugin.canServeUri(uri, homeDir); + } + } + return canServeUri; } /** - * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'. + * 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("/")) + if (tok.equals("/")) { newUri += "/"; - else if (tok.equals(" ")) + } else if (tok.equals(" ")) { newUri += "%20"; - else { + } else { try { newUri += URLEncoder.encode(tok, "UTF-8"); } catch (UnsupportedEncodingException ignored) { @@ -242,36 +253,125 @@ public class SimpleWebServer extends NanoHTTPD { return newUri; } - public Response serve(IHTTPSession session) { - Map<String, String> header = session.getHeaders(); - Map<String, String> parms = session.getParms(); - String uri = session.getUri(); + private String findIndexFileInDirectory(File directory) { + for (String fileName : SimpleWebServer.INDEX_FILE_NAMES) { + File indexFile = new File(directory, fileName); + if (indexFile.isFile()) { + return fileName; + } + } + return null; + } - if (!quiet) { - System.out.println(session.getMethod() + " '" + uri + "' "); + protected Response getForbiddenResponse(String s) { + return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s); + } - 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) + "'"); + protected Response getInternalErrorResponse(String s) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s); + } + + protected Response getNotFoundResponse() { + return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found."); + } + + /** + * Used to initialize and customize the server. + */ + public void init() { + } + + protected String listDirectory(String uri, File f) { + String heading = "Directory " + uri; + StringBuilder msg = + new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n" + + "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>"); + + String up = null; + if (uri.length() > 1) { + String u = uri.substring(0, uri.length() - 1); + int slash = u.lastIndexOf('/'); + if (slash >= 0 && slash < u.length()) { + up = uri.substring(0, slash + 1); } } - for (File homeDir : getRootDirs()) { - // Make sure we won't die of an exception later - if (!homeDir.isDirectory()) { - return getInternalErrorResponse("given path is not a directory (" + homeDir + ")."); + List<String> files = Arrays.asList(f.list(new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + return new File(dir, name).isFile(); + } + })); + Collections.sort(files); + List<String> directories = Arrays.asList(f.list(new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + return new File(dir, name).isDirectory(); + } + })); + Collections.sort(directories); + if (up != null || directories.size() + files.size() > 0) { + msg.append("<ul>"); + if (up != null || directories.size() > 0) { + msg.append("<section class=\"directories\">"); + if (up != null) { + msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>"); + } + for (String directory : directories) { + String dir = directory + "/"; + msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir) + .append("</span></a></b></li>"); + } + msg.append("</section>"); + } + if (files.size() > 0) { + msg.append("<section class=\"files\">"); + for (String file : files) { + msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>"); + File curFile = new File(f, file); + long len = curFile.length(); + msg.append(" <span class=\"filesize\">("); + if (len < 1024) { + msg.append(len).append(" bytes"); + } else if (len < 1024 * 1024) { + msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB"); + } else { + msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB"); + } + msg.append(")</span></li>"); + } + msg.append("</section>"); } + msg.append("</ul>"); } - return respond(Collections.unmodifiableMap(header), session, uri); + msg.append("</body></html>"); + return msg.toString(); + } + + public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) { + Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message); + response.addHeader("Accept-Ranges", "bytes"); + return response; } private Response respond(Map<String, String> headers, IHTTPSession session, String uri) { + // First let's handle CORS OPTION query + Response r; + if (cors != null && Method.OPTIONS.equals(session.getMethod())) { + r = new NanoHTTPD.Response(Response.Status.OK, MIME_PLAINTEXT, null, 0); + } else { + r = defaultRespond(headers, session, uri); + } + + if (cors != null) { + r = addCORSHeaders(headers, r, cors); + } + return r; + } + + private Response defaultRespond(Map<String, String> headers, IHTTPSession session, String uri) { // Remove URL arguments uri = uri.trim().replace(File.separatorChar, '/'); if (uri.indexOf('?') >= 0) { @@ -279,38 +379,39 @@ public class SimpleWebServer extends NanoHTTPD { } // Prohibit getting out of current directory - if (uri.startsWith("src/main") || uri.endsWith("src/main") || uri.contains("../")) { + if (uri.contains("../")) { return getForbiddenResponse("Won't serve ../ for security reasons."); } boolean canServeUri = false; File homeDir = null; - List<File> roots = getRootDirs(); - for (int i = 0; !canServeUri && i < roots.size(); i++) { - homeDir = roots.get(i); + for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) { + homeDir = this.rootDirs.get(i); canServeUri = canServeUri(uri, homeDir); } if (!canServeUri) { return getNotFoundResponse(); } - // Browsers get confused without '/' after the directory, send a redirect. + // Browsers get confused without '/' after the directory, send a + // redirect. File f = new File(homeDir, uri); if (f.isDirectory() && !uri.endsWith("/")) { uri += "/"; - Response res = createResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + - uri + "\">" + uri + "</a></body></html>"); + Response res = + newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri + "</a></body></html>"); res.addHeader("Location", uri); return res; } if (f.isDirectory()) { - // First look for index files (index.html, index.htm, etc) and if none found, list the directory if readable. + // First look for index files (index.html, index.htm, etc) and if + // none found, list the directory if readable. String indexFile = findIndexFileInDirectory(f); if (indexFile == null) { if (f.canRead()) { // No index file, list the directory if it is readable - return createResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f)); + return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f)); } else { return getForbiddenResponse("No directory listing."); } @@ -318,11 +419,10 @@ public class SimpleWebServer extends NanoHTTPD { return respond(headers, session, uri + indexFile); } } - String mimeTypeForFile = getMimeTypeForFile(uri); - WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile); + WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile); Response response = null; - if (plugin != null) { + if (plugin != null && plugin.canServeUri(uri, homeDir)) { response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile); if (response != null && response instanceof InternalRewrite) { InternalRewrite rewrite = (InternalRewrite) response; @@ -334,37 +434,39 @@ public class SimpleWebServer extends NanoHTTPD { return response != null ? response : getNotFoundResponse(); } - protected Response getNotFoundResponse() { - return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, - "Error 404, file not found."); - } + @Override + public Response serve(IHTTPSession session) { + Map<String, String> header = session.getHeaders(); + Map<String, String> parms = session.getParms(); + String uri = session.getUri(); - protected Response getForbiddenResponse(String s) { - return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " - + s); - } + if (!this.quiet) { + System.out.println(session.getMethod() + " '" + uri + "' "); - protected Response getInternalErrorResponse(String s) { - return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, - "INTERNAL ERRROR: " + s); - } + 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) + "'"); + } + } - private boolean canServeUri(String uri, File homeDir) { - boolean canServeUri; - File f = new File(homeDir, uri); - canServeUri = f.exists(); - if (!canServeUri) { - String mimeTypeForFile = getMimeTypeForFile(uri); - WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile); - if (plugin != null) { - canServeUri = plugin.canServeUri(uri, homeDir); + for (File homeDir : this.rootDirs) { + // Make sure we won't die of an exception later + if (!homeDir.isDirectory()) { + return getInternalErrorResponse("given path is not a directory (" + homeDir + ")."); } } - return canServeUri; + return respond(Collections.unmodifiableMap(header), session, uri); } /** - * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters. + * Serves file from homeDir and its' subdirectories (only). Uses only URI, + * ignores all headers and HTTP parameters. */ Response serveFile(String uri, Map<String, String> header, File file, String mime) { Response res; @@ -390,12 +492,27 @@ public class SimpleWebServer extends NanoHTTPD { } } - // Change return code and add Content-Range header when skipping is requested + // get if-range header. If present, it must match etag or else we + // should ignore the range request + String ifRange = header.get("if-range"); + boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange)); + + String ifNoneMatch = header.get("if-none-match"); + boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && (ifNoneMatch.equals("*") || ifNoneMatch.equals(etag)); + + // Change return code and add Content-Range header when skipping is + // requested long fileLen = file.length(); - if (range != null && startFrom >= 0) { - if (startFrom >= fileLen) { - res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); - res.addHeader("Content-Range", "bytes 0-0/" + fileLen); + + if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) { + // range request that matches current etag + // and the startFrom of the range is satisfiable + if (headerIfNoneMatchPresentAndMatching) { + // range request that matches current etag + // and the startFrom of the range is satisfiable + // would return range from file + // respond with not-modified + res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { if (endAt < 0) { @@ -406,25 +523,39 @@ public class SimpleWebServer extends NanoHTTPD { newLen = 0; } - final long dataLen = newLen; - FileInputStream fis = new FileInputStream(file) { - @Override - public int available() throws IOException { - return (int) dataLen; - } - }; + FileInputStream fis = new FileInputStream(file); fis.skip(startFrom); - res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis); - res.addHeader("Content-Length", "" + dataLen); + res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen); + res.addHeader("Accept-Ranges", "bytes"); + res.addHeader("Content-Length", "" + newLen); res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); res.addHeader("ETag", etag); } } else { - if (etag.equals(header.get("if-none-match"))) - res = createResponse(Response.Status.NOT_MODIFIED, mime, ""); - else { - res = createResponse(Response.Status.OK, mime, new FileInputStream(file)); + + if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) { + // return the size of the file + // 4xx responses are not trumped by if-none-match + res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); + res.addHeader("Content-Range", "bytes */" + fileLen); + res.addHeader("ETag", etag); + } else if (range == null && headerIfNoneMatchPresentAndMatching) { + // full-file-fetch request + // would return entire file + // respond with not-modified + res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); + res.addHeader("ETag", etag); + } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) { + // range request that doesn't match current etag + // would return entire (different) file + // respond with not-modified + + res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); + res.addHeader("ETag", etag); + } else { + // supply the file + res = newFixedFileResponse(file, mime); res.addHeader("Content-Length", "" + fileLen); res.addHeader("ETag", etag); } @@ -436,106 +567,37 @@ public class SimpleWebServer extends NanoHTTPD { return res; } - // Get MIME type from file name extension, if possible - private String getMimeTypeForFile(String uri) { - int dot = uri.lastIndexOf('.'); - String mime = null; - if (dot >= 0) { - mime = MIME_TYPES.get(uri.substring(dot + 1).toLowerCase()); - } - return mime == null ? MIME_DEFAULT_BINARY : mime; - } - - // Announce that the file server accepts partial content requests - private Response createResponse(Response.Status status, String mimeType, InputStream message) { - Response res = new Response(status, mimeType, message); + private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException { + Response res; + res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length()); res.addHeader("Accept-Ranges", "bytes"); return res; } - // Announce that the file server accepts partial content requests - private Response createResponse(Response.Status status, String mimeType, String message) { - Response res = new Response(status, mimeType, message); - res.addHeader("Accept-Ranges", "bytes"); - return res; + protected Response addCORSHeaders(Map<String, String> queryHeaders, Response resp, String cors) { + resp.addHeader("Access-Control-Allow-Origin", cors); + resp.addHeader("Access-Control-Allow-Headers", calculateAllowHeaders(queryHeaders)); + resp.addHeader("Access-Control-Allow-Credentials", "true"); + resp.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS); + resp.addHeader("Access-Control-Max-Age", "" + MAX_AGE); + + return resp; } - private String findIndexFileInDirectory(File directory) { - for (String fileName : INDEX_FILE_NAMES) { - File indexFile = new File(directory, fileName); - if (indexFile.exists()) { - return fileName; - } - } - return null; + private String calculateAllowHeaders(Map<String, String> queryHeaders) { + // here we should use the given asked headers + // but NanoHttpd uses a Map whereas it is possible for requester to send + // several time the same header + // let's just use default values for this version + return System.getProperty(ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, DEFAULT_ALLOWED_HEADERS); } - protected String listDirectory(String uri, File f) { - String heading = "Directory " + uri; - StringBuilder msg = new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + - "span.dirname { font-weight: bold; }\n" + - "span.filesize { font-size: 75%; }\n" + - "// -->\n" + - "</style>" + - "</head><body><h1>" + heading + "</h1>"); + private final static String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, HEAD"; - String up = null; - if (uri.length() > 1) { - String u = uri.substring(0, uri.length() - 1); - int slash = u.lastIndexOf('/'); - if (slash >= 0 && slash < u.length()) { - up = uri.substring(0, slash + 1); - } - } + private final static int MAX_AGE = 42 * 60 * 60; - List<String> files = Arrays.asList(f.list(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return new File(dir, name).isFile(); - } - })); - Collections.sort(files); - List<String> directories = Arrays.asList(f.list(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return new File(dir, name).isDirectory(); - } - })); - Collections.sort(directories); - if (up != null || directories.size() + files.size() > 0) { - msg.append("<ul>"); - if (up != null || directories.size() > 0) { - msg.append("<section class=\"directories\">"); - if (up != null) { - msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>"); - } - for (String directory : directories) { - String dir = directory + "/"; - msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir).append("</span></a></b></li>"); - } - msg.append("</section>"); - } - if (files.size() > 0) { - msg.append("<section class=\"files\">"); - for (String file : files) { - msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>"); - File curFile = new File(f, file); - long len = curFile.length(); - msg.append(" <span class=\"filesize\">("); - if (len < 1024) { - msg.append(len).append(" bytes"); - } else if (len < 1024 * 1024) { - msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB"); - } else { - msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10 % 100).append(" MB"); - } - msg.append(")</span></li>"); - } - msg.append("</section>"); - } - msg.append("</ul>"); - } - msg.append("</body></html>"); - return msg.toString(); - } + // explicitly relax visibility to package for tests purposes + final static String DEFAULT_ALLOWED_HEADERS = "origin,accept,content-type"; + + public final static String ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME = "AccessControlAllowHeader"; } |