diff --git a/pom.xml b/pom.xml
index 361b6e43..60f566fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,7 +41,8 @@
1.6
1.6
-
+
+
org.apache.maven.plugins
maven-shade-plugin
@@ -55,7 +56,9 @@
- org.dynmap:dynmap-api:jar:*
+ org.dynmap:dynmap-api:jar:*
+ org.eclipse.jetty:jetty-*:jar:*
+ javax.servlet:javax.servlet-api:jar:*
@@ -107,5 +110,21 @@
jar
compile
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+
+
+ org.eclipse.jetty
+ jetty-server
+ 8.0.1.v20110908
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ 8.0.1.v20110908
+
+
diff --git a/src/main/java/org/dynmap/DynmapPlugin.java b/src/main/java/org/dynmap/DynmapPlugin.java
index 41ba81fc..f780f8b8 100644
--- a/src/main/java/org/dynmap/DynmapPlugin.java
+++ b/src/main/java/org/dynmap/DynmapPlugin.java
@@ -70,13 +70,20 @@ import org.dynmap.permissions.BukkitPermissions;
import org.dynmap.permissions.NijikokunPermissions;
import org.dynmap.permissions.OpPermissions;
import org.dynmap.permissions.PermissionProvider;
-import org.dynmap.web.HttpServer;
-import org.dynmap.web.handlers.ClientConfigurationHandler;
-import org.dynmap.web.handlers.FilesystemHandler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
private String version;
- public HttpServer webServer = null;
+ private Server webServer = null;
+ private ServletContextHandler webServerContextHandler = null;
public MapManager mapManager = null;
public PlayerList playerList;
public ConfigurationNode configuration;
@@ -123,10 +130,6 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
return mapManager;
}
- public HttpServer getWebServer() {
- return webServer;
- }
-
/* Add/Replace branches in configuration tree with contribution from a separate file */
private void mergeConfigurationBranch(ConfigurationNode cfgnode, String branch, boolean replace_existing, boolean islist) {
Object srcbranch = cfgnode.getObject(branch);
@@ -409,50 +412,120 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
}
public void loadWebserver() {
- InetAddress bindAddress;
- {
- String address = configuration.getString("webserver-bindaddress", "0.0.0.0");
- try {
- bindAddress = address.equals("0.0.0.0")
- ? null
- : InetAddress.getByName(address);
- } catch (UnknownHostException e) {
- bindAddress = null;
- }
- }
- int port = configuration.getInteger("webserver-port", 8123);
+ webServer = new Server(new InetSocketAddress(configuration.getString("webserver-bindaddress", "0.0.0.0"), configuration.getInteger("webserver-port", 8123)));
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/");
+ webServer.setHandler(context);
+ webServerContextHandler = context;
+
boolean allow_symlinks = configuration.getBoolean("allow-symlinks", false);
- boolean checkbannedips = configuration.getBoolean("check-banned-ips", true);
int maxconnections = configuration.getInteger("max-sessions", 30);
if(maxconnections < 2) maxconnections = 2;
- /* Load customized response headers, if any */
- ConfigurationNode custhttp = configuration.getNode("http-response-headers");
- HashMap custhdrs = new HashMap();
- if(custhttp != null) {
- for(String k : custhttp.keySet()) {
- String v = custhttp.getString(k);
- if(v != null) {
- custhdrs.put(k, v);
- }
- }
- }
- HttpServer.setCustomHeaders(custhdrs);
-
+
if(allow_symlinks)
Log.verboseinfo("Web server is permitting symbolic links");
else
- Log.verboseinfo("Web server is not permitting symbolic links");
- webServer = new HttpServer(bindAddress, port, checkbannedips, maxconnections, this);
- webServer.handlers.put("/", new FilesystemHandler(getFile(configuration.getString("webpath", "web")), allow_symlinks));
- webServer.handlers.put("/tiles/", new FilesystemHandler(tilesDirectory, allow_symlinks));
- webServer.handlers.put("/up/configuration", new ClientConfigurationHandler(this));
+ Log.verboseinfo("Web server is not permitting symbolic links");
+
+ org.eclipse.jetty.server.Server s = new org.eclipse.jetty.server.Server();
+ ServletHandler handler = new org.eclipse.jetty.servlet.ServletHandler();
+ s.setHandler(handler);
+
+ /* Check for banned IPs */
+ boolean checkbannedips = configuration.getBoolean("check-banned-ips", true);
+ if (checkbannedips) {
+ context.addFilter(new FilterHolder(new Filter() {
+ private HashSet banned_ips = new HashSet();
+ private HashSet banned_ips_notified = new HashSet();
+ private long last_loaded = 0;
+ private long lastmod = 0;
+ private static final long BANNED_RELOAD_INTERVAL = 15000; /* Every 15 seconds */
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException { }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletResponse resp = (HttpServletResponse)response;
+ String ipaddr = request.getRemoteAddr();
+ if (isIpBanned(ipaddr)) {
+ Log.info("Rejected connection by banned IP address - " + ipaddr);
+ resp.sendError(403);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ private void loadBannedIPs() {
+ banned_ips.clear();
+ banned_ips_notified.clear();
+ banned_ips.addAll(getServer().getIPBans());
+ }
+
+ /* Return true if address is banned */
+ public boolean isIpBanned(String ipaddr) {
+ long t = System.currentTimeMillis();
+ if((t < last_loaded) || ((t-last_loaded) > BANNED_RELOAD_INTERVAL)) {
+ loadBannedIPs();
+ last_loaded = t;
+ }
+ if(banned_ips.contains(ipaddr)) {
+ if(!banned_ips_notified.contains(ipaddr)) {
+ banned_ips_notified.add(ipaddr);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void destroy() { }
+ }), "/*", null);
+ }
+
+ /* Load customized response headers, if any */
+ final ConfigurationNode custhttp = configuration.getNode("http-response-headers");
+ context.addFilter(new FilterHolder(new Filter() {
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException { }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletResponse resp = (HttpServletResponse)response;
+
+ if(custhttp != null) {
+ for(String k : custhttp.keySet()) {
+ String v = custhttp.getString(k);
+ if(v != null) {
+ resp.setHeader(k, v);
+ }
+ }
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy() { }
+ }), "/*", null);
+
+ addServlet("/*", new org.dynmap.servlet.FileServlet(getFile(getWebPath()).getAbsolutePath(), allow_symlinks));
+ addServlet("/tiles/*", new org.dynmap.servlet.FileServlet(tilesDirectory.getAbsolutePath(), allow_symlinks));
+ addServlet("/up/configuration", new org.dynmap.servlet.ClientConfigurationServlet(this));
+
}
+
+ public void addServlet(String path, HttpServlet servlet) {
+ ServletHolder holder = new ServletHolder(servlet);
+ webServerContextHandler.getServletHandler().addServletWithMapping(holder, path);
+ }
+
public void startWebserver() {
try {
- webServer.startServer();
- } catch (IOException e) {
- Log.severe("Failed to start WebServer on " + webServer.getAddress() + ":" + webServer.getPort() + "!");
+ webServer.start();
+ } catch (Exception e) {
+ Log.severe("Failed to start WebServer!", e);
}
}
@@ -475,7 +548,11 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
}
if (webServer != null) {
- webServer.shutdown();
+ try {
+ webServer.stop();
+ } catch (Exception e) {
+ Log.severe("Failed to stop WebServer!", e);
+ }
webServer = null;
}
/* Clean up all registered handlers */
diff --git a/src/main/java/org/dynmap/InternalClientUpdateComponent.java b/src/main/java/org/dynmap/InternalClientUpdateComponent.java
index 86e2439a..34e723fb 100644
--- a/src/main/java/org/dynmap/InternalClientUpdateComponent.java
+++ b/src/main/java/org/dynmap/InternalClientUpdateComponent.java
@@ -1,8 +1,7 @@
package org.dynmap;
-import org.dynmap.Event.Listener;
-import org.dynmap.web.handlers.ClientUpdateHandler;
-import org.dynmap.web.handlers.SendMessageHandler;
+import org.dynmap.servlet.ClientUpdateServlet;
+import org.dynmap.servlet.SendMessageServlet;
import org.json.simple.JSONObject;
import static org.dynmap.JSONUtils.*;
@@ -10,12 +9,11 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent {
public InternalClientUpdateComponent(final DynmapPlugin plugin, final ConfigurationNode configuration) {
super(plugin, configuration);
- final boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
- final boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false);
- final boolean trust_client_name = configuration.getBoolean("trustclientname", false);
- final boolean useplayerloginip = configuration.getBoolean("use-player-login-ip", true);
- final boolean checkuserban = configuration.getBoolean("block-banned-player-chat", true);
- final boolean requireplayerloginip = configuration.getBoolean("require-player-login-ip", false);
+ plugin.addServlet("/up/world/*", new ClientUpdateServlet(plugin));
+
+ final Boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
+ final Boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false);
+ final Boolean trust_client_name = configuration.getBoolean("trustclientname", false);
final float webchatInterval = configuration.getFloat("webchat-interval", 1);
final String spammessage = plugin.configuration.getString("spammessage", "You may only chat once every %interval% seconds.");
@@ -26,38 +24,31 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent {
s(t, "webchat-interval", webchatInterval);
}
});
-
- plugin.webServer.handlers.put("/up/", new ClientUpdateHandler(plugin));
-
+
if (allowwebchat) {
- SendMessageHandler messageHandler = new SendMessageHandler() {{
+ SendMessageServlet messageHandler = new SendMessageServlet() {{
maximumMessageInterval = (int)(webchatInterval * 1000);
spamMessage = "\""+spammessage+"\"";
hideip = hidewebchatip;
- this.plug_in = plugin;
this.trustclientname = trust_client_name;
- this.use_player_login_ip = useplayerloginip;
- this.require_player_login_ip = requireplayerloginip;
- this.check_user_ban = checkuserban;
- onMessageReceived.addListener(new Listener() {
+ onMessageReceived.addListener(new Event.Listener () {
@Override
public void triggered(Message t) {
webChat(t.name, t.message);
}
});
}};
-
- plugin.webServer.handlers.put("/up/sendmessage", messageHandler);
+ plugin.addServlet("/up/sendmessage", messageHandler);
}
}
-
+
protected void webChat(String name, String message) {
if(plugin.mapManager == null)
return;
// TODO: Change null to something meaningful.
plugin.mapManager.pushUpdate(new Client.ChatMessage("web", null, name, message, null));
Log.info(unescapeString(plugin.configuration.getString("webprefix", "\u00A72[WEB] ")) + name + ": " + unescapeString(plugin.configuration.getString("websuffix", "\u00A7f")) + message);
- ChatEvent event = new ChatEvent("web", name, message);
+ ChatEvent event = new ChatEvent("web", name, message);
plugin.events.trigger("webchat", event);
}
}
diff --git a/src/main/java/org/dynmap/servlet/ClientConfigurationServlet.java b/src/main/java/org/dynmap/servlet/ClientConfigurationServlet.java
new file mode 100644
index 00000000..9f21e69e
--- /dev/null
+++ b/src/main/java/org/dynmap/servlet/ClientConfigurationServlet.java
@@ -0,0 +1,52 @@
+package org.dynmap.servlet;
+
+import java.io.IOException;
+import java.util.Date;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.dynmap.DynmapPlugin;
+import org.dynmap.DynmapWorld;
+import org.dynmap.Event;
+import org.json.simple.JSONObject;
+
+public class ClientConfigurationServlet extends HttpServlet {
+ private static final long serialVersionUID = 9106801553080522469L;
+ private DynmapPlugin plugin;
+ private byte[] cachedConfiguration = null;
+ public ClientConfigurationServlet(DynmapPlugin plugin) {
+ this.plugin = plugin;
+ plugin.events.addListener("worldactivated", new Event.Listener() {
+ @Override
+ public void triggered(DynmapWorld t) {
+ cachedConfiguration = null;
+ }
+ });
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+ byte[] outputBytes = cachedConfiguration;
+ if (outputBytes == null) {
+ JSONObject json = new JSONObject();
+ plugin.events.trigger("buildclientconfiguration", json);
+
+ String s = json.toJSONString();
+
+ outputBytes = s.getBytes("UTF-8");
+ }
+ if (outputBytes != null) {
+ cachedConfiguration = outputBytes;
+ }
+ String dateStr = new Date().toString();
+ res.addHeader("Date", dateStr);
+ res.setContentType("text/plain; charset=utf-8");
+ res.addHeader("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
+ res.addHeader("Last-modified", dateStr);
+ res.setContentLength(outputBytes.length);
+ res.getOutputStream().write(outputBytes);
+ }
+}
diff --git a/src/main/java/org/dynmap/servlet/ClientUpdateServlet.java b/src/main/java/org/dynmap/servlet/ClientUpdateServlet.java
new file mode 100644
index 00000000..b3cb91da
--- /dev/null
+++ b/src/main/java/org/dynmap/servlet/ClientUpdateServlet.java
@@ -0,0 +1,74 @@
+package org.dynmap.servlet;
+
+import static org.dynmap.JSONUtils.s;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.dynmap.ClientUpdateEvent;
+import org.dynmap.DynmapPlugin;
+import org.dynmap.DynmapWorld;
+import org.dynmap.Log;
+import org.dynmap.web.HttpField;
+import org.json.simple.JSONObject;
+
+public class ClientUpdateServlet extends HttpServlet {
+ private DynmapPlugin plugin;
+
+ public ClientUpdateServlet(DynmapPlugin plugin) {
+ this.plugin = plugin;
+ }
+
+ Pattern updatePathPattern = Pattern.compile("/([^/]+)/([0-9]*)");
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ String path = req.getPathInfo();
+ Matcher match = updatePathPattern.matcher(path);
+
+ if (!match.matches()) {
+ resp.sendError(404, "World not found");
+ return;
+ }
+
+ String worldName = match.group(1);
+ String timeKey = match.group(2);
+
+ DynmapWorld dynmapWorld = null;
+ if(plugin.mapManager != null) {
+ dynmapWorld = plugin.mapManager.getWorld(worldName);
+ }
+ if (dynmapWorld == null || dynmapWorld.world == null) {
+ resp.sendError(404, "World not found");
+ return;
+ }
+ long current = System.currentTimeMillis();
+ long since = 0;
+
+ try {
+ since = Long.parseLong(timeKey);
+ } catch (NumberFormatException e) {
+ }
+
+ JSONObject u = new JSONObject();
+ s(u, "timestamp", current);
+ plugin.events.trigger("buildclientupdate", new ClientUpdateEvent(since, dynmapWorld, u));
+
+ byte[] bytes = u.toJSONString().getBytes("UTF-8");
+
+ String dateStr = new Date().toString();
+ resp.addHeader(HttpField.Date, dateStr);
+ resp.addHeader(HttpField.ContentType, "text/plain; charset=utf-8");
+ resp.addHeader(HttpField.Expires, "Thu, 01 Dec 1994 16:00:00 GMT");
+ resp.addHeader(HttpField.LastModified, dateStr);
+ resp.addHeader(HttpField.ContentLength, Integer.toString(bytes.length));
+
+ resp.getOutputStream().write(bytes);
+ }
+}
diff --git a/src/main/java/org/dynmap/servlet/FileServlet.java b/src/main/java/org/dynmap/servlet/FileServlet.java
new file mode 100644
index 00000000..8f4e34bd
--- /dev/null
+++ b/src/main/java/org/dynmap/servlet/FileServlet.java
@@ -0,0 +1,578 @@
+/*
+ * net/balusc/webapp/FileServlet.java
+ *
+ * Copyright (C) 2009 BalusC
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this library.
+ * If not, see .
+ */
+
+package org.dynmap.servlet;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.net.URLDecoder;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A file servlet supporting resume of downloads and client-side caching and GZIP of text content.
+ * This servlet can also be used for images, client-side caching would become more efficient.
+ * This servlet can also be used for text files, GZIP would decrease network bandwidth.
+ *
+ * @author BalusC
+ * @link http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html
+ */
+public class FileServlet extends HttpServlet {
+
+ // Constants ----------------------------------------------------------------------------------
+
+ private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
+ private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
+
+ // Properties ---------------------------------------------------------------------------------
+
+ private String basePath = null;
+ private boolean allow_symlinks = true;
+ private String[] indexFiles = new String[] {
+ "index.html"
+ };
+
+ // Actions ------------------------------------------------------------------------------------
+
+ public FileServlet() {
+ }
+
+ public FileServlet(String basePath, boolean allow_symlinks) {
+ this.basePath = new File(basePath).getAbsolutePath();
+ this.allow_symlinks = allow_symlinks;
+ }
+
+ /**
+ * Initialize the servlet.
+ * @see HttpServlet#init().
+ */
+ public void init() throws ServletException {
+ if (basePath == null) {
+ setBasePath(new File(getServletContext().getRealPath(getInitParameter("basePath"))).getAbsolutePath());
+ }
+ }
+
+ public void setBasePath(String basePath) {
+ // Validate base path.
+ if (basePath == null) {
+ throw new InvalidParameterException("'basePath' is required.");
+ } else {
+ File path = new File(basePath);
+ if (!path.exists()) {
+ throw new InvalidParameterException("'basePath' value '"
+ + basePath + "' does actually not exist in file system.");
+ } else if (!path.isDirectory()) {
+ throw new InvalidParameterException("'basePath' value '"
+ + basePath + "' is actually not a directory in file system.");
+ } else if (!path.canRead()) {
+ throw new InvalidParameterException("'basePath' value '"
+ + basePath + "' is actually not readable in file system.");
+ }
+ }
+ this.basePath = basePath;
+ }
+
+ /**
+ * Process HEAD request. This returns the same headers as GET request, but without content.
+ * @see HttpServlet#doHead(HttpServletRequest, HttpServletResponse).
+ */
+ protected void doHead(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException
+ {
+ // Process request without content.
+ processRequest(request, response, false);
+ }
+
+ /**
+ * Process GET request.
+ * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse).
+ */
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException
+ {
+ // Process request with content.
+ processRequest(request, response, true);
+ }
+
+ private static String getNormalizedPath(String p) {
+ p = p.replace('\\', '/');
+ String[] tok = p.split("/");
+ int i, j;
+ for(i = 0, j = 0; i < tok.length; i++) {
+ if((tok[i] == null) || (tok[i].length() == 0) || (tok[i].equals("."))) {
+ tok[i] = null;
+ }
+ else if(tok[i].equals("..")) {
+ if(j > 0) { j--; tok[j] = null; }
+ tok[i] = null;
+ }
+ else {
+ tok[j] = tok[i];
+ j++;
+ }
+ }
+ String path = "";
+ for(i = 0; i < j; i++) {
+ if(tok[i] != null)
+ path = path + "/" + tok[i];
+ }
+ return path;
+ }
+
+ /**
+ * Process the actual request.
+ * @param request The request to be processed.
+ * @param response The response to be created.
+ * @param content Whether the request body should be written (GET) or not (HEAD).
+ * @throws IOException If something fails at I/O level.
+ */
+ private void processRequest
+ (HttpServletRequest request, HttpServletResponse response, boolean content)
+ throws IOException
+ {
+ // Validate the requested file ------------------------------------------------------------
+
+ // Get requested file by path info.
+ String requestedFile = request.getPathInfo();
+
+ if (requestedFile != null)
+ requestedFile = getNormalizedPath(requestedFile);
+
+ // Check if file is actually supplied to the request URL.
+ if (requestedFile == null) {
+ // Do your thing if the file is not supplied to the request URL.
+ // Throw an exception, or send 404, or show default/warning page, or just ignore it.
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // URL-decode the file name (might contain spaces and on) and prepare file object.
+ File file = new File(basePath, URLDecoder.decode(requestedFile, "UTF-8"));
+
+ String fpath = null;
+ if(allow_symlinks)
+ fpath = file.getAbsolutePath();
+ else
+ fpath = file.getCanonicalPath();
+
+ if (!fpath.startsWith(basePath)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ if (file.isDirectory()) {
+ File directory = file;
+ for (int i = 0; i < indexFiles.length; i++) {
+ file = new File(directory, indexFiles[i]);
+ if (file.isFile())
+ break;
+ }
+ }
+
+ // Check if file actually exists in filesystem.
+ if (!file.exists()) {
+ // Do your thing if the file appears to be non-existing.
+ // Throw an exception, or send 404, or show default/warning page, or just ignore it.
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // Prepare some variables. The ETag is an unique identifier of the file.
+ String fileName = file.getName();
+ long length = file.length();
+ long lastModified = file.lastModified();
+ String eTag = fileName + "_" + length + "_" + lastModified;
+
+
+ // Validate request headers for caching ---------------------------------------------------
+
+ // If-None-Match header should contain "*" or ETag. If so, then return 304.
+ String ifNoneMatch = request.getHeader("If-None-Match");
+ if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
+ response.setHeader("ETag", eTag); // Required in 304.
+ response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+ return;
+ }
+
+ // If-Modified-Since header should be greater than LastModified. If so, then return 304.
+ // This header is ignored if any If-None-Match header is specified.
+ long ifModifiedSince = request.getDateHeader("If-Modified-Since");
+ if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
+ response.setHeader("ETag", eTag); // Required in 304.
+ response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+ return;
+ }
+
+
+ // Validate request headers for resume ----------------------------------------------------
+
+ // If-Match header should contain "*" or ETag. If not, then return 412.
+ String ifMatch = request.getHeader("If-Match");
+ if (ifMatch != null && !matches(ifMatch, eTag)) {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return;
+ }
+
+ // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
+ long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
+ if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return;
+ }
+
+
+ // Validate and process range -------------------------------------------------------------
+
+ // Prepare some variables. The full Range represents the complete file.
+ Range full = new Range(0, length - 1, length);
+ List ranges = new ArrayList();
+
+ // Validate and process Range and If-Range headers.
+ String range = request.getHeader("Range");
+ if (range != null) {
+
+ // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
+ if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
+ response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+
+ // If-Range header should either match ETag or be greater then LastModified. If not,
+ // then return full file.
+ String ifRange = request.getHeader("If-Range");
+ if (ifRange != null && !ifRange.equals(eTag)) {
+ try {
+ long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
+ if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
+ ranges.add(full);
+ }
+ } catch (IllegalArgumentException ignore) {
+ ranges.add(full);
+ }
+ }
+
+ // If any valid If-Range header, then process each part of byte range.
+ if (ranges.isEmpty()) {
+ String[] rangesParts = range.substring(6).split(",");
+
+ if (rangesParts.length > 1) {
+ response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+ for (String part : rangesParts) {
+ // Assuming a file with length of 100, the following examples returns bytes at:
+ // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
+ long start = sublong(part, 0, part.indexOf("-"));
+ long end = sublong(part, part.indexOf("-") + 1, part.length());
+
+ if (start == -1) {
+ start = length - end;
+ end = length - 1;
+ } else if (end == -1 || end > length - 1) {
+ end = length - 1;
+ }
+
+ // Check if Range is syntactically valid. If not, then return 416.
+ if (start > end) {
+ response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+
+ // Add range.
+ ranges.add(new Range(start, end, length));
+ }
+ }
+ }
+
+
+ // Prepare and initialize response --------------------------------------------------------
+
+ // Get content type by file name and set default GZIP support and content disposition.
+ boolean acceptsGzip = false;
+ String disposition = "inline";
+
+ String contentType = getContentType(fileName);
+
+ // If content type is text, then determine whether GZIP content encoding is supported by
+ // the browser and expand content type with the one and right character encoding.
+ if (contentType.startsWith("text")) {
+ String acceptEncoding = request.getHeader("Accept-Encoding");
+ acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");
+ contentType += ";charset=UTF-8";
+ }
+ // Else, expect for images, determine content disposition. If content type is supported by
+ // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
+ else if (!contentType.startsWith("image")) {
+ String accept = request.getHeader("Accept");
+ disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
+ }
+
+ // Initialize response.
+ response.reset();
+ response.setBufferSize(DEFAULT_BUFFER_SIZE);
+ response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("ETag", eTag);
+ response.setDateHeader("Last-Modified", lastModified);
+
+
+ // Send requested file (part(s)) to client ------------------------------------------------
+
+ // Prepare streams.
+ RandomAccessFile input = null;
+ OutputStream output = null;
+
+ try {
+ // Open streams.
+ input = new RandomAccessFile(file, "r");
+ output = response.getOutputStream();
+
+ if (ranges.isEmpty() || ranges.get(0) == full) {
+
+ // Return full file.
+ Range r = full;
+ response.setContentType(contentType);
+ response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
+
+ if (content) {
+ if (acceptsGzip) {
+ // The browser accepts GZIP, so GZIP the content.
+ response.setHeader("Content-Encoding", "gzip");
+ output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
+ } else {
+ // Content length is not directly predictable in case of GZIP.
+ // So only add it if there is no means of GZIP, else browser will hang.
+ response.setHeader("Content-Length", String.valueOf(r.length));
+ }
+
+ // Copy full range.
+ copy(input, output, r.start, r.length);
+ }
+
+ } else if (ranges.size() == 1) {
+
+ // Return single part of file.
+ Range r = ranges.get(0);
+ response.setContentType(contentType);
+ response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
+ response.setHeader("Content-Length", String.valueOf(r.length));
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
+
+ if (content) {
+ // Copy single part range.
+ copy(input, output, r.start, r.length);
+ }
+
+ } else {
+
+ // Return multiple parts of file.
+ response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
+
+ if (content) {
+ // Cast back to ServletOutputStream to get the easy println methods.
+ ServletOutputStream sos = (ServletOutputStream) output;
+
+ // Copy multi part range.
+ for (Range r : ranges) {
+ // Add multipart boundary and header fields for every range.
+ sos.println();
+ sos.println("--" + MULTIPART_BOUNDARY);
+ sos.println("Content-Type: " + contentType);
+ sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
+
+ // Copy single part range of multi part range.
+ copy(input, output, r.start, r.length);
+ }
+
+ // End with multipart boundary.
+ sos.println();
+ sos.println("--" + MULTIPART_BOUNDARY + "--");
+ }
+ }
+ } finally {
+ // Gently close streams.
+ close(output);
+ close(input);
+ }
+ }
+
+ // Helpers (can be refactored to public utility class) ----------------------------------------
+
+
+ final static Map mimeTypes = new HashMap() {{
+ this.put(".html", "text/html");
+ this.put(".htm", "text/html");
+ this.put(".js", "text/javascript");
+ this.put(".png", "image/png");
+ this.put(".jpg", "image/jpeg");
+ this.put(".css", "text/css");
+ this.put(".txt", "text/plain");
+ }};
+ public String getContentType(String fileName) {
+ // Don't use getServetContext!
+ /*String contentType = getServletContext().getMimeType(fileName);
+ */
+ String contentType = null;
+ int i = fileName.lastIndexOf('.');
+ if (i >= 0) {
+ String extension = fileName.substring(i);
+ contentType = mimeTypes.get(extension);
+ }
+
+ if (contentType == null) {
+ contentType = "application/octet-stream";
+ }
+
+ return contentType;
+ }
+
+ /**
+ * Returns true if the given accept header accepts the given value.
+ * @param acceptHeader The accept header.
+ * @param toAccept The value to be accepted.
+ * @return True if the given accept header accepts the given value.
+ */
+ private static boolean accepts(String acceptHeader, String toAccept) {
+ String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
+ Arrays.sort(acceptValues);
+ return Arrays.binarySearch(acceptValues, toAccept) > -1
+ || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
+ || Arrays.binarySearch(acceptValues, "*/*") > -1;
+ }
+
+ /**
+ * Returns true if the given match header matches the given value.
+ * @param matchHeader The match header.
+ * @param toMatch The value to be matched.
+ * @return True if the given match header matches the given value.
+ */
+ private static boolean matches(String matchHeader, String toMatch) {
+ String[] matchValues = matchHeader.split("\\s*,\\s*");
+ Arrays.sort(matchValues);
+ return Arrays.binarySearch(matchValues, toMatch) > -1
+ || Arrays.binarySearch(matchValues, "*") > -1;
+ }
+
+ /**
+ * Returns a substring of the given string value from the given begin index to the given end
+ * index as a long. If the substring is empty, then -1 will be returned
+ * @param value The string value to return a substring as long for.
+ * @param beginIndex The begin index of the substring to be returned as long.
+ * @param endIndex The end index of the substring to be returned as long.
+ * @return A substring of the given string value as long or -1 if substring is empty.
+ */
+ private static long sublong(String value, int beginIndex, int endIndex) {
+ String substring = value.substring(beginIndex, endIndex);
+ return (substring.length() > 0) ? Long.parseLong(substring) : -1;
+ }
+
+ /**
+ * Copy the given byte range of the given input to the given output.
+ * @param input The input to copy the given range to the given output for.
+ * @param output The output to copy the given range from the given input for.
+ * @param start Start of the byte range.
+ * @param length Length of the byte range.
+ * @throws IOException If something fails at I/O level.
+ */
+ private static void copy(RandomAccessFile input, OutputStream output, long start, long length)
+ throws IOException
+ {
+ byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ int read;
+
+ if (input.length() == length) {
+ // Write full range.
+ while ((read = input.read(buffer)) > 0) {
+ output.write(buffer, 0, read);
+ }
+ } else {
+ // Write partial range.
+ input.seek(start);
+ long toRead = length;
+
+ while ((read = input.read(buffer)) > 0) {
+ if ((toRead -= read) > 0) {
+ output.write(buffer, 0, read);
+ } else {
+ output.write(buffer, 0, (int) toRead + read);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Close the given resource.
+ * @param resource The resource to be closed.
+ */
+ private static void close(Closeable resource) {
+ if (resource != null) {
+ try {
+ resource.close();
+ } catch (IOException ignore) {
+ // Ignore IOException. If you want to handle this anyway, it might be useful to know
+ // that this will generally only be thrown when the client aborted the request.
+ }
+ }
+ }
+
+ // Inner classes ------------------------------------------------------------------------------
+
+ /**
+ * This class represents a byte range.
+ */
+ protected class Range {
+ long start;
+ long end;
+ long length;
+ long total;
+
+ /**
+ * Construct a byte range.
+ * @param start Start of the byte range.
+ * @param end End of the byte range.
+ * @param total Total length of the byte source.
+ */
+ public Range(long start, long end, long total) {
+ this.start = start;
+ this.end = end;
+ this.length = end - start + 1;
+ this.total = total;
+ }
+
+ }
+
+}
+
diff --git a/src/main/java/org/dynmap/servlet/JSONServlet.java b/src/main/java/org/dynmap/servlet/JSONServlet.java
new file mode 100644
index 00000000..7aea11e1
--- /dev/null
+++ b/src/main/java/org/dynmap/servlet/JSONServlet.java
@@ -0,0 +1,18 @@
+package org.dynmap.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONAware;
+import org.json.simple.JSONStreamAware;
+
+public class JSONServlet {
+ public static void respond(HttpServletResponse response, JSONStreamAware json) throws IOException {
+ response.setContentType("application/json");
+ PrintWriter writer = response.getWriter();
+ json.writeJSONString(writer);
+ writer.close();
+ }
+}
diff --git a/src/main/java/org/dynmap/servlet/MainServlet.java b/src/main/java/org/dynmap/servlet/MainServlet.java
new file mode 100644
index 00000000..26b2ca89
--- /dev/null
+++ b/src/main/java/org/dynmap/servlet/MainServlet.java
@@ -0,0 +1,146 @@
+package org.dynmap.servlet;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.dynmap.Log;
+
+public class MainServlet extends HttpServlet {
+ public static class Header {
+ public String name;
+ public String value;
+ public Header(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+ }
+
+ private static class Registration {
+ public String pattern;
+ public HttpServlet servlet;
+
+ public Registration(String pattern, HttpServlet servlet) {
+ this.pattern = pattern;
+ this.servlet = servlet;
+ }
+ }
+
+ List registrations = new LinkedList();
+ public List customHeaders = new LinkedList();
+
+ public void addServlet(String pattern, HttpServlet servlet) {
+ registrations.add(new Registration(pattern, servlet));
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ HashMap properties = new HashMap();
+ String path = req.getPathInfo();
+
+ for(Header header : customHeaders) {
+ resp.setHeader(header.name, header.value);
+ }
+
+ Registration bestMatch = null;
+ String bestMatchPart = null;
+ HashMap bestProperties = null;
+
+ for (Registration r : registrations) {
+ String matchingPart = match(r.pattern, path, properties);
+ if (matchingPart != null) {
+ if (bestMatchPart == null || bestMatchPart.length() < matchingPart.length()) {
+ bestMatch = r;
+ bestMatchPart = matchingPart;
+ bestProperties = properties;
+ properties = new HashMap();
+ }
+ }
+ }
+ if (bestMatch == null) {
+ resp.sendError(404);
+ } else {
+ String leftOverPath = path.substring(bestMatchPart.length());
+ HttpServletRequest newreq = new RequestWrapper(req, leftOverPath);
+ for(String key : bestProperties.keySet()) {
+ newreq.setAttribute(key, bestProperties.get(key));
+ }
+ bestMatch.servlet.service(newreq, resp);
+ }
+ }
+
+ public String match(String pattern, String path, Map properties) {
+ int patternStart = 0;
+ int pathStart = 0;
+ while (patternStart < pattern.length()) {
+ if (pattern.charAt(patternStart) == '{') {
+ // Found a variable.
+ int endOfVariable = pattern.indexOf('}', patternStart+1);
+ String variableName = pattern.substring(patternStart+1, endOfVariable);
+
+ int endOfSection = indexOfAny(path, new char[] { '/', '?' }, pathStart);
+ if (endOfSection < 0) {
+ endOfSection = path.length();
+ }
+ String variableValue = path.substring(pathStart, endOfSection);
+
+ // Store variable.
+ properties.put(variableName, variableValue);
+
+ patternStart = endOfVariable+1;
+ pathStart = endOfSection;
+ } else {
+ int endOfLiteral = pattern.indexOf('{', patternStart);
+ if (endOfLiteral < 0) {
+ endOfLiteral = pattern.length();
+ }
+ String literal = pattern.substring(patternStart, endOfLiteral);
+ int endOfPathLiteral = pathStart + literal.length();
+ if (endOfPathLiteral > path.length()) {
+ return null;
+ }
+ String matchingLiteral = path.substring(pathStart, endOfPathLiteral);
+ if (!literal.equals(matchingLiteral)) {
+ return null;
+ }
+
+ patternStart = endOfLiteral;
+ pathStart = endOfPathLiteral;
+ }
+ }
+ // Return the part of the url that matches the pattern. (if the pattern does not contain any variables, this will be equal to the pattern)
+ return path.substring(0, pathStart);
+ }
+
+ private int indexOfAny(String s, char[] cs, int startIndex) {
+ for(int i = startIndex; i < s.length(); i++) {
+ char c = s.charAt(i);
+ for(int j = 0; j < cs.length; j++) {
+ if (c == cs[j]) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ class RequestWrapper extends HttpServletRequestWrapper {
+ String pathInfo;
+ public RequestWrapper(HttpServletRequest request, String pathInfo) {
+ super(request);
+ this.pathInfo = pathInfo;
+ }
+ @Override
+ public String getPathInfo() {
+ return pathInfo;
+ }
+ }
+}
diff --git a/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java b/src/main/java/org/dynmap/servlet/SendMessageServlet.java
similarity index 60%
rename from src/main/java/org/dynmap/web/handlers/SendMessageHandler.java
rename to src/main/java/org/dynmap/servlet/SendMessageServlet.java
index 088cc79c..f6b2efc7 100644
--- a/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java
+++ b/src/main/java/org/dynmap/servlet/SendMessageServlet.java
@@ -1,5 +1,19 @@
-package org.dynmap.web.handlers;
+package org.dynmap.servlet;
+import org.bukkit.OfflinePlayer;
+import org.dynmap.DynmapPlugin;
+import org.dynmap.Event;
+import org.dynmap.Log;
+import org.dynmap.web.HttpStatus;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.HashMap;
@@ -7,96 +21,81 @@ import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
-import org.bukkit.OfflinePlayer;
-import org.dynmap.DynmapPlugin;
-import org.dynmap.Event;
-import org.dynmap.Log;
-import org.dynmap.web.HttpField;
-import org.dynmap.web.HttpHandler;
-import org.dynmap.web.HttpMethod;
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-import org.dynmap.web.HttpStatus;
-import org.json.simple.JSONObject;
-import org.json.simple.parser.JSONParser;
-
-public class SendMessageHandler implements HttpHandler {
+public class SendMessageServlet extends HttpServlet {
protected static final Logger log = Logger.getLogger("Minecraft");
private static final JSONParser parser = new JSONParser();
- public Event onMessageReceived = new Event();
+ public Event onMessageReceived = new Event();
private Charset cs_utf8 = Charset.forName("UTF-8");
public int maximumMessageInterval = 1000;
public boolean hideip = false;
public boolean trustclientname = false;
- public boolean use_player_login_ip = false;
- public boolean require_player_login_ip = false;
- public boolean check_user_ban = false;
- public DynmapPlugin plug_in;
+
public String spamMessage = "\"You may only chat once every %interval% seconds.\"";
private HashMap disallowedUsers = new HashMap();
private LinkedList disallowedUserQueue = new LinkedList();
private Object disallowedUsersLock = new Object();
private HashMap useralias = new HashMap();
private int aliasindex = 1;
-
+ public boolean use_player_login_ip = false;
+ public boolean require_player_login_ip = false;
+ public boolean check_user_ban = false;
+ public DynmapPlugin plug_in;
+
+
@Override
- public void handle(String path, HttpRequest request, HttpResponse response) throws Exception {
- if (!request.method.equals(HttpMethod.Post)) {
- response.status = HttpStatus.MethodNotAllowed;
- response.fields.put(HttpField.Accept, HttpMethod.Post);
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ InputStreamReader reader = new InputStreamReader(request.getInputStream(), cs_utf8);
+
+ JSONObject o = null;
+ try {
+ o = (JSONObject)parser.parse(reader);
+ } catch (ParseException e) {
+ response.sendError(HttpStatus.BadRequest.getCode());
return;
}
- InputStreamReader reader = new InputStreamReader(request.body, cs_utf8);
-
- JSONObject o = (JSONObject)parser.parse(reader);
final Message message = new Message();
-
+
message.name = "";
if(trustclientname) {
message.name = String.valueOf(o.get("name"));
}
boolean isip = true;
if((message.name == null) || message.name.equals("")) {
- /* If proxied client address, get original */
- if(request.fields.containsKey("X-Forwarded-For"))
- message.name = request.fields.get("X-Forwarded-For");
- /* If from loopback, we're probably getting from proxy - need to trust client */
- else if(request.rmtaddr.getAddress().isLoopbackAddress())
- message.name = String.valueOf(o.get("name"));
- else
- message.name = request.rmtaddr.getAddress().getHostAddress();
+ /* If proxied client address, get original */
+ if(request.getHeader("X-Forwarded-For") != null)
+ message.name = request.getHeader("X-Forwarded-For");
+ /* If from loopback, we're probably getting from proxy - need to trust client */
+ else if(request.getRemoteAddr() == "127.0.0.1")
+ message.name = String.valueOf(o.get("name"));
+ else
+ message.name = request.getRemoteAddr();
}
- if(use_player_login_ip) {
+ if (use_player_login_ip) {
List ids = plug_in.getIDsForIP(message.name);
- if(ids != null) {
+ if (ids != null) {
String id = ids.get(0);
- if(check_user_ban) {
+ if (check_user_ban) {
OfflinePlayer p = plug_in.getServer().getOfflinePlayer(id);
- if((p != null) && p.isBanned()) {
+ if ((p != null) && p.isBanned()) {
Log.info("Ignore message from '" + message.name + "' - banned player (" + id + ")");
- response.fields.put("Content-Length", "0");
- response.status = HttpStatus.Forbidden;
- response.getBody();
+ response.sendError(HttpStatus.Forbidden.getCode());
return;
}
}
message.name = ids.get(0);
isip = false;
- }
- else if(require_player_login_ip) {
+ } else if (require_player_login_ip) {
Log.info("Ignore message from '" + message.name + "' - no matching player login recorded");
- response.fields.put("Content-Length", "0");
- response.status = HttpStatus.Forbidden;
- response.getBody();
+ response.sendError(HttpStatus.Forbidden.getCode());
return;
}
}
- if(hideip && isip) { /* If hiding IP, find or assign alias */
- synchronized(disallowedUsersLock) {
+ if (hideip && isip) { /* If hiding IP, find or assign alias */
+ synchronized (disallowedUsersLock) {
String n = useralias.get(message.name);
- if(n == null) { /* Make ID */
+ if (n == null) { /* Make ID */
n = String.format("web-%03d", aliasindex);
aliasindex++;
useralias.put(message.name, n);
@@ -108,7 +107,7 @@ public class SendMessageHandler implements HttpHandler {
final long now = System.currentTimeMillis();
- synchronized(disallowedUsersLock) {
+ synchronized (disallowedUsersLock) {
// Allow users that user that are now allowed to send messages.
while (!disallowedUserQueue.isEmpty()) {
WebUser wu = disallowedUserQueue.getFirst();
@@ -122,25 +121,21 @@ public class SendMessageHandler implements HttpHandler {
WebUser user = disallowedUsers.get(message.name);
if (user == null) {
- user = new WebUser() {{
- name = message.name;
- nextMessageTime = now+maximumMessageInterval;
- }};
+ user = new WebUser() {
+ {
+ name = message.name;
+ nextMessageTime = now + maximumMessageInterval;
+ }
+ };
disallowedUsers.put(user.name, user);
disallowedUserQueue.add(user);
} else {
- response.fields.put("Content-Length", "0");
- response.status = HttpStatus.Forbidden;
- response.getBody();
+ response.sendError(HttpStatus.Forbidden.getCode());
return;
}
}
onMessageReceived.trigger(message);
-
- response.fields.put(HttpField.ContentLength, "0");
- response.status = HttpStatus.OK;
- response.getBody();
}
public static class Message {
diff --git a/src/main/java/org/dynmap/web/HttpHandler.java b/src/main/java/org/dynmap/web/HttpHandler.java
deleted file mode 100644
index 274217dd..00000000
--- a/src/main/java/org/dynmap/web/HttpHandler.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.dynmap.web;
-
-
-public interface HttpHandler {
- void handle(String path, HttpRequest request, HttpResponse response) throws Exception;
-}
diff --git a/src/main/java/org/dynmap/web/HttpRequest.java b/src/main/java/org/dynmap/web/HttpRequest.java
deleted file mode 100644
index dc8db7bb..00000000
--- a/src/main/java/org/dynmap/web/HttpRequest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.dynmap.web;
-
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.net.InetSocketAddress;
-
-public class HttpRequest {
- public String method;
- public String path;
- public String version;
- public Map fields = new HashMap();
- public InputStream body;
- public InetSocketAddress rmtaddr;
-}
diff --git a/src/main/java/org/dynmap/web/HttpResponse.java b/src/main/java/org/dynmap/web/HttpResponse.java
deleted file mode 100644
index 5b0c80b8..00000000
--- a/src/main/java/org/dynmap/web/HttpResponse.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.dynmap.web;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-public class HttpResponse {
- private HttpServerConnection connection;
- public String version = "1.1";
- public HttpStatus status = null;
- public Map fields = new HashMap();
-
- private OutputStream body;
- public OutputStream getBody() throws IOException {
- if (body != null) {
- connection.writeResponseHeader(this);
- OutputStream b = body;
- body = null;
- return b;
- }
- return null;
- }
-
- public HttpResponse(HttpServerConnection connection, OutputStream body) {
- this.connection = connection;
- this.body = body;
- }
-}
diff --git a/src/main/java/org/dynmap/web/HttpServer.java b/src/main/java/org/dynmap/web/HttpServer.java
deleted file mode 100644
index 9261b76a..00000000
--- a/src/main/java/org/dynmap/web/HttpServer.java
+++ /dev/null
@@ -1,195 +0,0 @@
-package org.dynmap.web;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.logging.Logger;
-
-import org.bukkit.plugin.Plugin;
-import org.dynmap.Log;
-
-public class HttpServer extends Thread {
- protected static final Logger log = Logger.getLogger("Minecraft");
-
- private ServerSocket sock = null;
- private Thread listeningThread;
-
- private InetAddress bindAddress;
- private int port;
- private boolean check_banned_ips;
- private int max_sessions;
-
- public SortedMap handlers = new TreeMap(Collections.reverseOrder());
-
- private Object lock = new Object();
- private HashSet active_connections = new HashSet();
- private HashSet keepalive_connections = new HashSet();
- private Plugin plugin;
- private static Map headers = new HashMap();
-
- public HttpServer(InetAddress bindAddress, int port, boolean check_banned_ips, int max_sessions, Plugin plg) {
- this.bindAddress = bindAddress;
- this.port = port;
- this.check_banned_ips = check_banned_ips;
- this.max_sessions = max_sessions;
- this.plugin = plg;
- }
-
- public InetAddress getAddress() {
- return bindAddress;
- }
-
- public int getPort() {
- return port;
- }
-
- public void startServer() throws IOException {
- sock = new ServerSocket(port, 50, bindAddress); /* 5 too low - more than a couple users during render will get connect errors on some tile loads */
- listeningThread = this;
- start();
- Log.info("Dynmap WebServer started on " + bindAddress + ":" + port);
- }
-
- public void run() {
- try {
- ServerSocket s = sock;
- while (listeningThread == Thread.currentThread()) {
- try {
- Socket socket = s.accept();
- if(checkForBannedIp(socket.getRemoteSocketAddress())) {
- try { socket.close(); } catch (IOException iox) {}
- socket = null;
- }
-
- HttpServerConnection requestThread = new HttpServerConnection(socket, this);
- synchronized(lock) {
- active_connections.add(requestThread);
- requestThread.start();
- /* If we're at limit, wait here until we're free to accept another */
- while((listeningThread == Thread.currentThread()) &&
- (active_connections.size() >= max_sessions)) {
- lock.wait(500);
- }
- }
- } catch (IOException e) {
- if(listeningThread != null) /* Only report this if we didn't initiate the shutdown */
- Log.info("map WebServer.run() stops with IOException");
- break;
- }
- }
- Log.info("Webserver shut down.");
- } catch (Exception ex) {
- Log.severe("Exception on WebServer-thread", ex);
- }
- }
-
- public void shutdown() {
- Log.info("Shutting down webserver...");
- listeningThread = null;
- try {
- if (sock != null) {
- sock.close();
- sock = null;
- }
- /* And kill off the active connections */
- HashSet sc;
- synchronized(lock) {
- sc = new HashSet(active_connections);
- }
- for(HttpServerConnection c : sc) {
- c.shutdownConnection();
- }
- } catch (IOException e) {
- Log.warning("Exception while closing socket for webserver shutdown", e);
- }
- }
-
- public boolean canKeepAlive(HttpServerConnection c) {
- synchronized(lock) {
- /* If less than half of our limit are keep-alive, approve */
- if(keepalive_connections.size() < (max_sessions/2)) {
- keepalive_connections.add(c);
- return true;
- }
- }
- return false;
- }
-
- public void connectionEnded(HttpServerConnection c) {
- synchronized(lock) {
- active_connections.remove(c);
- keepalive_connections.remove(c);
- lock.notifyAll();
- }
- }
-
- private HashSet banned_ips = new HashSet();
- private HashSet banned_ips_notified = new HashSet();
- private long last_loaded = 0;
- private long lastmod = 0;
- private static final long BANNED_RELOAD_INTERVAL = 15000; /* Every 15 seconds */
-
- private void loadBannedIPs() {
- banned_ips.clear();
- banned_ips_notified.clear();
- banned_ips.addAll(plugin.getServer().getIPBans());
- }
-
- /* Return true if address is banned */
- public boolean checkForBannedIp(SocketAddress socketAddress) {
- if(!check_banned_ips)
- return false;
-
- long t = System.currentTimeMillis();
- if((t < last_loaded) || ((t-last_loaded) > BANNED_RELOAD_INTERVAL)) {
- loadBannedIPs();
- last_loaded = t;
- }
- /* Follow same technique as MC uses - toString the SocketAddress and clip out string between "/" and ":" */
- String ip = socketAddress.toString();
- ip = ip.substring(ip.indexOf("/") + 1);
- ip = ip.substring(0, ip.indexOf(":"));
- if(banned_ips.contains(ip)) {
- if(banned_ips_notified.contains(ip) == false) {
- Log.info("Rejected connection by banned IP address - " + socketAddress.toString());
- banned_ips_notified.add(ip);
- }
- return true;
- }
- return false;
- }
- /* Return true if address is banned */
- public boolean checkForBannedIp(String ipaddr) {
- if(!check_banned_ips)
- return false;
-
- long t = System.currentTimeMillis();
- if((t < last_loaded) || ((t-last_loaded) > BANNED_RELOAD_INTERVAL)) {
- loadBannedIPs();
- last_loaded = t;
- }
- if(banned_ips.contains(ipaddr)) {
- if(banned_ips_notified.contains(ipaddr) == false) {
- Log.info("Rejected connection by banned IP address - " + ipaddr);
- banned_ips_notified.add(ipaddr);
- }
- return true;
- }
- return false;
- }
-
- public static Map getCustomHeaders() {
- return headers;
- }
- public static void setCustomHeaders(Map hdrs) {
- headers = hdrs;
- }
-}
diff --git a/src/main/java/org/dynmap/web/HttpServerConnection.java b/src/main/java/org/dynmap/web/HttpServerConnection.java
deleted file mode 100644
index 814f7ff5..00000000
--- a/src/main/java/org/dynmap/web/HttpServerConnection.java
+++ /dev/null
@@ -1,282 +0,0 @@
-package org.dynmap.web;
-
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.StringWriter;
-import java.net.Socket;
-import java.net.URLDecoder;
-import java.util.Map.Entry;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.dynmap.Log;
-import org.dynmap.debug.Debug;
-import java.net.InetSocketAddress;
-
-public class HttpServerConnection extends Thread {
- protected static final Logger log = Logger.getLogger("Minecraft");
-
- private static Pattern requestHeaderLine = Pattern.compile("^(\\S+)\\s+(\\S+)\\s+HTTP/(.+)$");
- private static Pattern requestHeaderField = Pattern.compile("^([^:]+):\\s*(.+)$");
-
- private Socket socket;
- private HttpServer server;
- private boolean do_shutdown;
- private boolean can_keepalive;
-
- private PrintStream printOut;
- private StringWriter sw = new StringWriter();
- private Matcher requestHeaderLineMatcher;
- private Matcher requestHeaderFieldMatcher;
-
- public HttpServerConnection(Socket socket, HttpServer server) {
- this.socket = socket;
- this.server = server;
- do_shutdown = false;
- can_keepalive = false;
- }
-
- private final static void readLine(InputStream in, StringWriter sw) throws IOException {
- int readc;
- while((readc = in.read()) > 0) {
- char c = (char)readc;
- if (c == '\n')
- break;
- else if (c != '\r')
- sw.append(c);
- }
- }
-
- private final String readLine(InputStream in) throws IOException {
- readLine(in, sw);
- String r = sw.toString();
- sw.getBuffer().setLength(0);
- return r;
- }
-
- private final boolean readRequestHeader(InputStream in, HttpRequest request) throws IOException {
- String statusLine = readLine(in);
-
- if (statusLine == null)
- return false;
-
- if (requestHeaderLineMatcher == null) {
- requestHeaderLineMatcher = requestHeaderLine.matcher(statusLine);
- } else {
- requestHeaderLineMatcher.reset(statusLine);
- }
-
- Matcher m = requestHeaderLineMatcher;
- if (!m.matches())
- return false;
- request.method = m.group(1);
- request.path = m.group(2);
- request.version = m.group(3);
-
- String line;
- while (!(line = readLine(in)).equals("")) {
- if (requestHeaderFieldMatcher == null) {
- requestHeaderFieldMatcher = requestHeaderField.matcher(line);
- } else {
- requestHeaderFieldMatcher.reset(line);
- }
-
- m = requestHeaderFieldMatcher;
- // Warning: unknown lines are ignored.
- if (m.matches()) {
- String fieldName = m.group(1);
- String fieldValue = m.group(2);
- // TODO: Does not support duplicate field-names.
- request.fields.put(fieldName, fieldValue);
- }
- }
- return true;
- }
-
- public static final void writeResponseHeader(PrintStream out, HttpResponse response) throws IOException {
- out.append("HTTP/");
- out.append(response.version);
- out.append(" ");
- out.append(String.valueOf(response.status.getCode()));
- out.append(" ");
- out.append(response.status.getText());
- out.append("\r\n");
- for (Entry field : response.fields.entrySet()) {
- out.append(field.getKey());
- out.append(": ");
- out.append(field.getValue());
- out.append("\r\n");
- }
- for(Entry custom : HttpServer.getCustomHeaders().entrySet()) {
- out.append(custom.getKey());
- out.append(": ");
- out.append(custom.getValue());
- out.append("\r\n");
- }
- out.append("\r\n");
- out.flush();
- }
-
- public final void writeResponseHeader(HttpResponse response) throws IOException {
- writeResponseHeader(printOut, response);
- }
-
- public void run() {
- try {
- if (socket == null)
- return;
- socket.setSoTimeout(5000);
- socket.setTcpNoDelay(true);
- InetSocketAddress rmtaddr = (InetSocketAddress)socket.getRemoteSocketAddress(); /* Get remote address */
- InputStream in = socket.getInputStream();
- BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream(), 40960);
-
- printOut = new PrintStream(out, false);
- while (true) {
- /* Check for start of each request - kicks out persistent connections */
- if(server.checkForBannedIp(rmtaddr)) {
- return;
- }
-
- HttpRequest request = new HttpRequest();
- request.rmtaddr = rmtaddr;
- if (!readRequestHeader(in, request)) {
- return;
- }
- String fwd_for = request.fields.get("X-Forwarded-For");
- if(fwd_for != null) {
- String[] ff = fwd_for.split(",");
- for(int i = 0; i < ff.length; i++) {
- if(server.checkForBannedIp(ff[i]))
- return;
- }
- }
-
- long bound = -1;
- BoundInputStream boundBody = null;
- {
- String contentLengthStr = request.fields.get(HttpField.ContentLength);
- if (contentLengthStr != null) {
- try {
- bound = Long.parseLong(contentLengthStr);
- } catch (NumberFormatException e) {
- }
- if (bound >= 0) {
- request.body = boundBody = new BoundInputStream(in, bound);
- } else {
- request.body = in;
- }
- }
- }
- boolean iskeepalive = false;
- String keepalive = request.fields.get(HttpField.Connection);
- if((keepalive != null) && (keepalive.toLowerCase().indexOf("keep-alive") >= 0)) {
- /* See if we're clear to do keepalive */
- if(!iskeepalive)
- iskeepalive = server.canKeepAlive(this);
- }
-
- // TODO: Optimize HttpHandler-finding by using a real path-aware tree.
- HttpHandler handler = null;
- String relativePath = null;
- for (Entry entry : server.handlers.entrySet()) {
- String key = entry.getKey();
- boolean directoryHandler = key.endsWith("/");
- if (directoryHandler && request.path.startsWith(entry.getKey()) || !directoryHandler && request.path.equals(entry.getKey())) {
- relativePath = request.path.substring(entry.getKey().length());
- relativePath = URLDecoder.decode(relativePath,"utf-8");
- handler = entry.getValue();
- break;
- }
- /* Wildcard handler for non-directory matches */
- else if(key.endsWith("*") && request.path.startsWith(key.substring(0, key.length()-1))) { relativePath = request.path.substring(entry.getKey().length());
- relativePath = request.path.substring(entry.getKey().length()-1);
- relativePath = URLDecoder.decode(relativePath,"utf-8");
- handler = entry.getValue();
- break;
- }
- }
-
- if (handler == null) {
- return;
- }
-
- HttpResponse response = new HttpResponse(this, out);
-
- if(iskeepalive) {
- response.fields.put(HttpField.Connection, "keep-alive");
- response.fields.put("Keep-Alive", "timeout=5");
- }
- else {
- response.fields.put(HttpField.Connection, "close");
- }
- try {
- handler.handle(relativePath, request, response);
- } catch (IOException e) {
- throw e;
- } catch (Exception e) {
- Log.severe("HttpHandler '" + handler + "' has thown an exception", e);
- out.flush();
- return;
- }
-
- if (bound > 0 && boundBody.skip(bound) < bound) {
- Debug.debug("Incoming stream was only read partially by handler '" + handler + "'.");
- //socket.close();
- //return;
- }
-
- boolean isKeepalive = iskeepalive && !"close".equals(request.fields.get(HttpField.Connection)) && !"close".equals(response.fields.get(HttpField.Connection));
- String contentLength = response.fields.get("Content-Length");
- if (isKeepalive && contentLength == null) {
- // A handler has been a bad boy, but we're here to fix it.
- response.fields.put("Content-Length", "0");
- OutputStream responseBody = response.getBody();
-
- // The HttpHandler has already send the headers and written to the body without setting the Content-Length.
- if (responseBody == null) {
- Debug.debug("Response was given without Content-Length by '" + handler + "' for path '" + request.path + "'.");
- out.flush();
- return;
- }
- }
-
- out.flush();
-
- if (!isKeepalive) {
- return;
- }
- }
- } catch (IOException e) {
-
- } catch (Exception e) {
- if(!do_shutdown) {
- Log.severe("Exception while handling request: ", e);
- e.printStackTrace();
- }
- } finally {
- if (socket != null) {
- try {
- socket.close();
- } catch (IOException ex) {
- }
- }
- server.connectionEnded(this);
- }
- }
- public void shutdownConnection() {
- try {
- do_shutdown = true;
- if(socket != null) {
- socket.close();
- }
- join(); /* Wait for thread to die */
- } catch (IOException iox) {
- } catch (InterruptedException ix) {
- }
- }
-}
diff --git a/src/main/java/org/dynmap/web/handlers/ClientConfigurationHandler.java b/src/main/java/org/dynmap/web/handlers/ClientConfigurationHandler.java
deleted file mode 100644
index 9a899f07..00000000
--- a/src/main/java/org/dynmap/web/handlers/ClientConfigurationHandler.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.dynmap.web.handlers;
-
-import java.io.BufferedOutputStream;
-import java.util.Date;
-import org.dynmap.DynmapPlugin;
-import org.dynmap.DynmapWorld;
-import org.dynmap.Event;
-import org.dynmap.web.HttpHandler;
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-import org.dynmap.web.HttpStatus;
-import org.json.simple.JSONObject;
-
-public class ClientConfigurationHandler implements HttpHandler {
- private DynmapPlugin plugin;
- private byte[] cachedConfiguration = null;
- public ClientConfigurationHandler(DynmapPlugin plugin) {
- this.plugin = plugin;
- plugin.events.addListener("worldactivated", new Event.Listener() {
- @Override
- public void triggered(DynmapWorld t) {
- cachedConfiguration = null;
- }
- });
- }
- @Override
- public void handle(String path, HttpRequest request, HttpResponse response) throws Exception {
- if (cachedConfiguration == null) {
- JSONObject configurationObject = new JSONObject();
- plugin.events.trigger("buildclientconfiguration", configurationObject);
-
- String s = configurationObject.toJSONString();
-
- cachedConfiguration = s.getBytes("UTF-8");
- }
- String dateStr = new Date().toString();
-
- response.fields.put("Date", dateStr);
- response.fields.put("Content-Type", "text/plain; charset=utf-8");
- response.fields.put("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
- response.fields.put("Last-modified", dateStr);
- response.fields.put("Content-Length", Integer.toString(cachedConfiguration.length));
- response.status = HttpStatus.OK;
-
- BufferedOutputStream out = null;
- out = new BufferedOutputStream(response.getBody());
- out.write(cachedConfiguration);
- out.flush();
- }
-}
diff --git a/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java b/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java
deleted file mode 100644
index 9c6ab99f..00000000
--- a/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.dynmap.web.handlers;
-
-import java.io.BufferedOutputStream;
-import java.util.Date;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.dynmap.ClientUpdateEvent;
-import org.dynmap.DynmapPlugin;
-import org.dynmap.DynmapWorld;
-import org.dynmap.web.HttpField;
-import org.dynmap.web.HttpHandler;
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-import org.dynmap.web.HttpStatus;
-import org.json.simple.JSONObject;
-import static org.dynmap.JSONUtils.*;
-
-public class ClientUpdateHandler implements HttpHandler {
- private DynmapPlugin plugin;
-
- public ClientUpdateHandler(DynmapPlugin plugin) {
- this.plugin = plugin;
- }
-
- Pattern updatePathPattern = Pattern.compile("world/([^/]+)/([0-9]*)");
- private static final HttpStatus WorldNotFound = new HttpStatus(HttpStatus.NotFound.getCode(), "World Not Found");
- @Override
- public void handle(String path, HttpRequest request, HttpResponse response) throws Exception {
-
- Matcher match = updatePathPattern.matcher(path);
-
- if (!match.matches()) {
- response.status = HttpStatus.Forbidden;
- return;
- }
-
- String worldName = match.group(1);
- String timeKey = match.group(2);
-
- DynmapWorld dynmapWorld = null;
- if(plugin.mapManager != null) {
- dynmapWorld = plugin.mapManager.getWorld(worldName);
- }
- if (dynmapWorld == null || dynmapWorld.world == null) {
- response.status = WorldNotFound;
- return;
- }
- long current = System.currentTimeMillis();
- long since = 0;
-
- if (path.length() > 0) {
- try {
- since = Long.parseLong(timeKey);
- } catch (NumberFormatException e) {
- }
- }
-
- JSONObject u = new JSONObject();
- s(u, "timestamp", current);
- plugin.events.trigger("buildclientupdate", new ClientUpdateEvent(since, dynmapWorld, u));
-
- byte[] bytes = u.toJSONString().getBytes("UTF-8");
-
- String dateStr = new Date().toString();
- response.fields.put(HttpField.Date, dateStr);
- response.fields.put(HttpField.ContentType, "text/plain; charset=utf-8");
- response.fields.put(HttpField.Expires, "Thu, 01 Dec 1994 16:00:00 GMT");
- response.fields.put(HttpField.LastModified, dateStr);
- response.fields.put(HttpField.ContentLength, Integer.toString(bytes.length));
- response.status = HttpStatus.OK;
-
- BufferedOutputStream out = null;
- out = new BufferedOutputStream(response.getBody());
- out.write(bytes);
- out.flush();
- }
-}
diff --git a/src/main/java/org/dynmap/web/handlers/FileHandler.java b/src/main/java/org/dynmap/web/handlers/FileHandler.java
deleted file mode 100644
index e00ea5dc..00000000
--- a/src/main/java/org/dynmap/web/handlers/FileHandler.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package org.dynmap.web.handlers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.logging.Logger;
-
-import org.dynmap.web.HttpField;
-import org.dynmap.web.HttpHandler;
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-import org.dynmap.web.HttpStatus;
-
-public abstract class FileHandler implements HttpHandler {
- protected static final Logger log = Logger.getLogger("Minecraft");
-
- private LinkedList bufferpool = new LinkedList();
- private Object lock = new Object();
- private static final int MAX_FREE_IN_POOL = 2;
-
- private static Map mimes = new HashMap();
- static {
- mimes.put(".html", "text/html");
- mimes.put(".htm", "text/html");
- mimes.put(".js", "text/javascript");
- mimes.put(".png", "image/png");
- mimes.put(".jpg", "image/jpeg");
- mimes.put(".css", "text/css");
- mimes.put(".txt", "text/plain");
- }
-
- public static final String getMimeTypeFromExtension(String extension) {
- String m = mimes.get(extension);
- if (m != null)
- return m;
- return "application/octet-steam";
- }
-
- protected abstract InputStream getFileInput(String path, HttpRequest request, HttpResponse response);
-
- protected void closeFileInput(String path, InputStream in) throws IOException {
- in.close();
- }
-
- protected String getExtension(String path) {
- int dotindex = path.lastIndexOf('.');
- if (dotindex > 0)
- return path.substring(dotindex);
- return null;
- }
-
- protected final String formatPath(String path) {
- int qmark = path.indexOf('?');
- if (qmark >= 0)
- path = path.substring(0, qmark);
-
- if (path.startsWith("/") || path.startsWith("."))
- return null;
- if (path.length() == 0)
- path = getDefaultFilename(path);
- return path;
- }
-
- protected String getDefaultFilename(String path) {
- return path + "index.html";
- }
-
- private byte[] allocateReadBuffer() {
- byte[] buf;
- synchronized(lock) {
- buf = bufferpool.poll();
- }
- if(buf == null) {
- buf = new byte[40960];
- }
- return buf;
- }
-
- private void freeReadBuffer(byte[] buf) {
- synchronized(lock) {
- if(bufferpool.size() < MAX_FREE_IN_POOL)
- bufferpool.push(buf);
- }
- }
-
- @Override
- public void handle(String path, HttpRequest request, HttpResponse response) throws Exception {
- InputStream fileInput = null;
- try {
- path = formatPath(path);
- fileInput = getFileInput(path, request, response);
- if (fileInput == null) {
- response.status = HttpStatus.NotFound;
- return;
- }
-
- String extension = getExtension(path);
- String mimeType = getMimeTypeFromExtension(extension);
-
- response.fields.put(HttpField.ContentType, mimeType);
- response.status = HttpStatus.OK;
- OutputStream out = response.getBody();
- byte[] readBuffer = allocateReadBuffer();
- try {
- int readBytes;
- while ((readBytes = fileInput.read(readBuffer)) > 0) {
- out.write(readBuffer, 0, readBytes);
- }
- } finally {
- freeReadBuffer(readBuffer);
- }
- } finally {
- if (fileInput != null) {
- try { closeFileInput(path, fileInput); fileInput = null; } catch (IOException ex) { }
- }
- }
- }
-}
diff --git a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java b/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java
deleted file mode 100644
index beb27125..00000000
--- a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.dynmap.web.handlers;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-
-import org.dynmap.Log;
-import org.dynmap.utils.FileLockManager;
-import org.dynmap.web.HttpField;
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-
-
-public class FilesystemHandler extends FileHandler {
- private File root;
- private boolean allow_symlinks = false;
- private String root_path;
- public FilesystemHandler(File root, boolean allow_symlinks) {
- if (!root.isDirectory())
- throw new IllegalArgumentException();
- this.root = root;
- this.allow_symlinks = allow_symlinks;
- this.root_path = root.getAbsolutePath();
- }
- @Override
- protected InputStream getFileInput(String path, HttpRequest request, HttpResponse response) {
- if(path == null) return null;
- path = getNormalizedPath(path); /* Resolve out relative stuff - nothing allowed above webroot */
- File file = new File(root, path);
- if(!file.isFile())
- return null;
- if(!FileLockManager.getReadLock(file, 5000)) { /* Wait up to 5 seconds for lock */
- Log.severe("Timeout waiting for lock on file " + file.getPath());
- return null;
- }
- FileInputStream result = null;
- try {
- String fpath;
- if(allow_symlinks)
- fpath = file.getAbsolutePath();
- else
- fpath = file.getCanonicalPath();
- if (fpath.startsWith(root_path)) {
- try {
- result = new FileInputStream(file);
- } catch (FileNotFoundException e) {
- return null;
- }
- response.fields.put(HttpField.ContentLength, Long.toString(file.length()));
- return result;
- }
- } catch(IOException ex) {
- Log.severe("Unable to get canoical path of requested file.", ex);
- } finally {
- if(result == null) FileLockManager.releaseReadLock(file);
- }
- return null;
- }
- protected void closeFileInput(String path, InputStream in) throws IOException {
- path = getNormalizedPath(path);
- try {
- super.closeFileInput(path, in);
- } finally {
- File file = new File(root, path);
- FileLockManager.releaseReadLock(file);
- }
- }
- public static String getNormalizedPath(String p) {
- p = p.replace('\\', '/');
- String[] tok = p.split("/");
- int i, j;
- for(i = 0, j = 0; i < tok.length; i++) {
- if((tok[i] == null) || (tok[i].length() == 0) || (tok[i].equals("."))) {
- tok[i] = null;
- }
- else if(tok[i].equals("..")) {
- if(j > 0) { j--; tok[j] = null; }
- tok[i] = null;
- }
- else {
- tok[j] = tok[i];
- j++;
- }
- }
- String path = "";
- for(i = 0; i < j; i++) {
- if(tok[i] != null)
- path = path + "/" + tok[i];
- }
- return path;
- }
-}
diff --git a/src/main/java/org/dynmap/web/handlers/JarFileHandler.java b/src/main/java/org/dynmap/web/handlers/JarFileHandler.java
deleted file mode 100644
index c822fa59..00000000
--- a/src/main/java/org/dynmap/web/handlers/JarFileHandler.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.dynmap.web.handlers;
-
-import java.io.InputStream;
-
-import org.dynmap.web.HttpRequest;
-import org.dynmap.web.HttpResponse;
-
-
-public class JarFileHandler extends FileHandler {
- private String root;
- public JarFileHandler(String root) {
- if (root.endsWith("/")) root = root.substring(0, root.length()-1);
- this.root = root;
- }
- @Override
- protected InputStream getFileInput(String path, HttpRequest request, HttpResponse response) {
- return this.getClass().getResourceAsStream(root + "/" + path);
- }
-}