diff --git a/src/main/java/me/totalfreedom/totalfreedommod/httpd/module/Module_file.java b/src/main/java/me/totalfreedom/totalfreedommod/httpd/module/Module_file.java new file mode 100644 index 00000000..c8f09c15 --- /dev/null +++ b/src/main/java/me/totalfreedom/totalfreedommod/httpd/module/Module_file.java @@ -0,0 +1,382 @@ +package me.totalfreedom.totalfreedommod.httpd.module; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import me.totalfreedom.totalfreedommod.TotalFreedomMod; +import me.totalfreedom.totalfreedommod.config.ConfigEntry; +import me.totalfreedom.totalfreedommod.httpd.HTTPDaemon; +import me.totalfreedom.totalfreedommod.httpd.NanoHTTPD; +import me.totalfreedom.totalfreedommod.httpd.NanoHTTPD.Response; +import org.apache.commons.lang3.StringUtils; + +/* + * This class was adapted from https://github.com/NanoHttpd/nanohttpd/blob/master/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java + */ +public class Module_file extends HTTPDModule +{ + + private final File rootDir = new File(ConfigEntry.HTTPD_PUBLIC_FOLDER.getString()); + public static final Map MIME_TYPES = new HashMap<>(); + + static + { + MIME_TYPES.put("css", "text/css"); + MIME_TYPES.put("htm", "text/html"); + MIME_TYPES.put("html", "text/html"); + MIME_TYPES.put("xml", "text/xml"); + MIME_TYPES.put("java", "text/x-java-source, text/java"); + MIME_TYPES.put("txt", "text/plain"); + MIME_TYPES.put("asc", "text/plain"); + MIME_TYPES.put("yml", "text/yaml"); + MIME_TYPES.put("gif", "image/gif"); + MIME_TYPES.put("jpg", "image/jpeg"); + MIME_TYPES.put("jpeg", "image/jpeg"); + MIME_TYPES.put("png", "image/png"); + MIME_TYPES.put("mp3", "audio/mpeg"); + MIME_TYPES.put("m3u", "audio/mpeg-url"); + MIME_TYPES.put("mp4", "video/mp4"); + MIME_TYPES.put("ogv", "video/ogg"); + MIME_TYPES.put("flv", "video/x-flv"); + MIME_TYPES.put("mov", "video/quicktime"); + MIME_TYPES.put("swf", "application/x-shockwave-flash"); + MIME_TYPES.put("js", "application/javascript"); + MIME_TYPES.put("pdf", "application/pdf"); + MIME_TYPES.put("doc", "application/msword"); + MIME_TYPES.put("ogg", "application/x-ogg"); + MIME_TYPES.put("zip", "application/octet-stream"); + MIME_TYPES.put("exe", "application/octet-stream"); + MIME_TYPES.put("class", "application/octet-stream"); + } + + public Module_file(TotalFreedomMod plugin, NanoHTTPD.HTTPSession session) + { + super(plugin, session); + } + + private File getRootDir() + { + return rootDir; + } + + 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; + } + + public Response serveFile(String uri, Map params, File homeDir) + { + Response res = null; + + // Make sure we won't die of an exception later + if (!homeDir.isDirectory()) + { + res = new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.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.contains("../")) + { + res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons."); + } + } + + File f = new File(homeDir, uri); + if (res == null && !f.exists()) + { + res = new Response(Response.Status.NOT_FOUND, NanoHTTPD.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(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "Redirected: " + uri + + ""); + 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"); + } + else if (f.canRead()) + { + // No index file, list the directory if it is readable + res = new Response(listDirectory(uri, f)); + } + else + { + res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: No directory listing."); + } + } + } + + 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 = HTTPDaemon.MIME_DEFAULT_BINARY; + } + + // Calculate etag + String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode()); + + final long fileLen = f.length(); + + long startFrom = 0; + long endAt = -1; + final String range = params.get("range"); + if (range != null) + { + final String[] rangeParams = StringUtils.split(range, "="); + if (rangeParams.length >= 2) + { + if ("bytes".equalsIgnoreCase(rangeParams[0])) + { + try + { + int minus = rangeParams[1].indexOf('-'); + if (minus > 0) + { + startFrom = Long.parseLong(rangeParams[1].substring(0, minus)); + endAt = Long.parseLong(rangeParams[1].substring(minus + 1)); + } + } + catch (NumberFormatException ignored) + { + } + } + else if ("tail".equalsIgnoreCase(rangeParams[0])) + { + try + { + final long tailLen = Long.parseLong(rangeParams[1]); + if (tailLen < fileLen) + { + startFrom = fileLen - tailLen - 2; + if (startFrom < 0) + { + startFrom = 0; + } + } + } + catch (NumberFormatException ignored) + { + } + } + } + } + + // Change return code and add Content-Range header when skipping is requested + if (range != null && startFrom >= 0) + { + if (startFrom >= fileLen) + { + res = new Response(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.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(Response.Status.PARTIAL_CONTENT, mime, fis); + res.addHeader("Content-Length", "" + dataLen); + res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); + res.addHeader("ETag", etag); + } + } + else + { + res = new Response(Response.Status.OK, mime, new FileInputStream(f)); + res.addHeader("Content-Length", "" + fileLen); + res.addHeader("ETag", etag); + } + } + } + catch (IOException ioe) + { + res = new Response(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: Reading file failed."); + } + + res.addHeader("Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes + return res; + } + + private String listDirectory(String uri, File f) + { + String heading = "Directory " + uri; + String msg = "" + heading + "" + + "

" + heading + "

"; + + 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); + } + } + + List 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 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 += "
    "; + if (up != null || directories.size() > 0) + { + msg += "
    "; + if (up != null) + { + msg += "
  • ..
  • "; + } + for (int i = 0; i < directories.size(); i++) + { + String dir = directories.get(i) + "/"; + msg += "
  • " + dir + "
  • "; + } + msg += "
    "; + } + if (files.size() > 0) + { + msg += "
    "; + for (int i = 0; i < files.size(); i++) + { + String file = files.get(i); + + msg += "
  • " + file + ""; + File curFile = new File(f, file); + long len = curFile.length(); + msg += " ("; + 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 += ")
  • "; + } + msg += "
    "; + } + msg += "
"; + } + msg += ""; + return msg; + } + + @Override + public Response getResponse() + { + return serveFile(uri, params, getRootDir()); + } +}