mirror of
https://github.com/webbukkit/dynmap.git
synced 2024-11-28 05:05:16 +01:00
Replaced webserver with Jetty.
This commit is contained in:
parent
db3ab5a437
commit
3c4a88a874
23
pom.xml
23
pom.xml
@ -41,7 +41,8 @@
|
|||||||
<source>1.6</source>
|
<source>1.6</source>
|
||||||
<target>1.6</target>
|
<target>1.6</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
@ -55,7 +56,9 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<artifactSet>
|
<artifactSet>
|
||||||
<includes>
|
<includes>
|
||||||
<include>org.dynmap:dynmap-api:jar:*</include>
|
<include>org.dynmap:dynmap-api:jar:*</include>
|
||||||
|
<include>org.eclipse.jetty:jetty-*:jar:*</include>
|
||||||
|
<include>javax.servlet:javax.servlet-api:jar:*</include>
|
||||||
</includes>
|
</includes>
|
||||||
</artifactSet>
|
</artifactSet>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -107,5 +110,21 @@
|
|||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>3.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>8.0.1.v20110908</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-servlet</artifactId>
|
||||||
|
<version>8.0.1.v20110908</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
@ -70,13 +70,20 @@ import org.dynmap.permissions.BukkitPermissions;
|
|||||||
import org.dynmap.permissions.NijikokunPermissions;
|
import org.dynmap.permissions.NijikokunPermissions;
|
||||||
import org.dynmap.permissions.OpPermissions;
|
import org.dynmap.permissions.OpPermissions;
|
||||||
import org.dynmap.permissions.PermissionProvider;
|
import org.dynmap.permissions.PermissionProvider;
|
||||||
import org.dynmap.web.HttpServer;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.dynmap.web.handlers.ClientConfigurationHandler;
|
import org.eclipse.jetty.servlet.FilterHolder;
|
||||||
import org.dynmap.web.handlers.FilesystemHandler;
|
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 {
|
public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
|
||||||
private String version;
|
private String version;
|
||||||
public HttpServer webServer = null;
|
private Server webServer = null;
|
||||||
|
private ServletContextHandler webServerContextHandler = null;
|
||||||
public MapManager mapManager = null;
|
public MapManager mapManager = null;
|
||||||
public PlayerList playerList;
|
public PlayerList playerList;
|
||||||
public ConfigurationNode configuration;
|
public ConfigurationNode configuration;
|
||||||
@ -123,10 +130,6 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
|
|||||||
return mapManager;
|
return mapManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpServer getWebServer() {
|
|
||||||
return webServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add/Replace branches in configuration tree with contribution from a separate file */
|
/* Add/Replace branches in configuration tree with contribution from a separate file */
|
||||||
private void mergeConfigurationBranch(ConfigurationNode cfgnode, String branch, boolean replace_existing, boolean islist) {
|
private void mergeConfigurationBranch(ConfigurationNode cfgnode, String branch, boolean replace_existing, boolean islist) {
|
||||||
Object srcbranch = cfgnode.getObject(branch);
|
Object srcbranch = cfgnode.getObject(branch);
|
||||||
@ -409,50 +412,120 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loadWebserver() {
|
public void loadWebserver() {
|
||||||
InetAddress bindAddress;
|
webServer = new Server(new InetSocketAddress(configuration.getString("webserver-bindaddress", "0.0.0.0"), configuration.getInteger("webserver-port", 8123)));
|
||||||
{
|
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
|
||||||
String address = configuration.getString("webserver-bindaddress", "0.0.0.0");
|
context.setContextPath("/");
|
||||||
try {
|
webServer.setHandler(context);
|
||||||
bindAddress = address.equals("0.0.0.0")
|
webServerContextHandler = context;
|
||||||
? null
|
|
||||||
: InetAddress.getByName(address);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
bindAddress = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int port = configuration.getInteger("webserver-port", 8123);
|
|
||||||
boolean allow_symlinks = configuration.getBoolean("allow-symlinks", false);
|
boolean allow_symlinks = configuration.getBoolean("allow-symlinks", false);
|
||||||
boolean checkbannedips = configuration.getBoolean("check-banned-ips", true);
|
|
||||||
int maxconnections = configuration.getInteger("max-sessions", 30);
|
int maxconnections = configuration.getInteger("max-sessions", 30);
|
||||||
if(maxconnections < 2) maxconnections = 2;
|
if(maxconnections < 2) maxconnections = 2;
|
||||||
/* Load customized response headers, if any */
|
|
||||||
ConfigurationNode custhttp = configuration.getNode("http-response-headers");
|
|
||||||
HashMap<String, String> custhdrs = new HashMap<String,String>();
|
|
||||||
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)
|
if(allow_symlinks)
|
||||||
Log.verboseinfo("Web server is permitting symbolic links");
|
Log.verboseinfo("Web server is permitting symbolic links");
|
||||||
else
|
else
|
||||||
Log.verboseinfo("Web server is not permitting symbolic links");
|
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));
|
org.eclipse.jetty.server.Server s = new org.eclipse.jetty.server.Server();
|
||||||
webServer.handlers.put("/tiles/", new FilesystemHandler(tilesDirectory, allow_symlinks));
|
ServletHandler handler = new org.eclipse.jetty.servlet.ServletHandler();
|
||||||
webServer.handlers.put("/up/configuration", new ClientConfigurationHandler(this));
|
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<String> banned_ips = new HashSet<String>();
|
||||||
|
private HashSet<String> banned_ips_notified = new HashSet<String>();
|
||||||
|
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() {
|
public void startWebserver() {
|
||||||
try {
|
try {
|
||||||
webServer.startServer();
|
webServer.start();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Log.severe("Failed to start WebServer on " + webServer.getAddress() + ":" + webServer.getPort() + "!");
|
Log.severe("Failed to start WebServer!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,7 +548,11 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (webServer != null) {
|
if (webServer != null) {
|
||||||
webServer.shutdown();
|
try {
|
||||||
|
webServer.stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.severe("Failed to stop WebServer!", e);
|
||||||
|
}
|
||||||
webServer = null;
|
webServer = null;
|
||||||
}
|
}
|
||||||
/* Clean up all registered handlers */
|
/* Clean up all registered handlers */
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package org.dynmap;
|
package org.dynmap;
|
||||||
|
|
||||||
import org.dynmap.Event.Listener;
|
import org.dynmap.servlet.ClientUpdateServlet;
|
||||||
import org.dynmap.web.handlers.ClientUpdateHandler;
|
import org.dynmap.servlet.SendMessageServlet;
|
||||||
import org.dynmap.web.handlers.SendMessageHandler;
|
|
||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
import static org.dynmap.JSONUtils.*;
|
import static org.dynmap.JSONUtils.*;
|
||||||
|
|
||||||
@ -10,12 +9,11 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent {
|
|||||||
|
|
||||||
public InternalClientUpdateComponent(final DynmapPlugin plugin, final ConfigurationNode configuration) {
|
public InternalClientUpdateComponent(final DynmapPlugin plugin, final ConfigurationNode configuration) {
|
||||||
super(plugin, configuration);
|
super(plugin, configuration);
|
||||||
final boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
|
plugin.addServlet("/up/world/*", new ClientUpdateServlet(plugin));
|
||||||
final boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false);
|
|
||||||
final boolean trust_client_name = configuration.getBoolean("trustclientname", false);
|
final Boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
|
||||||
final boolean useplayerloginip = configuration.getBoolean("use-player-login-ip", true);
|
final Boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false);
|
||||||
final boolean checkuserban = configuration.getBoolean("block-banned-player-chat", true);
|
final Boolean trust_client_name = configuration.getBoolean("trustclientname", false);
|
||||||
final boolean requireplayerloginip = configuration.getBoolean("require-player-login-ip", false);
|
|
||||||
final float webchatInterval = configuration.getFloat("webchat-interval", 1);
|
final float webchatInterval = configuration.getFloat("webchat-interval", 1);
|
||||||
final String spammessage = plugin.configuration.getString("spammessage", "You may only chat once every %interval% seconds.");
|
final String spammessage = plugin.configuration.getString("spammessage", "You may only chat once every %interval% seconds.");
|
||||||
|
|
||||||
@ -27,27 +25,20 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
plugin.webServer.handlers.put("/up/", new ClientUpdateHandler(plugin));
|
|
||||||
|
|
||||||
if (allowwebchat) {
|
if (allowwebchat) {
|
||||||
SendMessageHandler messageHandler = new SendMessageHandler() {{
|
SendMessageServlet messageHandler = new SendMessageServlet() {{
|
||||||
maximumMessageInterval = (int)(webchatInterval * 1000);
|
maximumMessageInterval = (int)(webchatInterval * 1000);
|
||||||
spamMessage = "\""+spammessage+"\"";
|
spamMessage = "\""+spammessage+"\"";
|
||||||
hideip = hidewebchatip;
|
hideip = hidewebchatip;
|
||||||
this.plug_in = plugin;
|
|
||||||
this.trustclientname = trust_client_name;
|
this.trustclientname = trust_client_name;
|
||||||
this.use_player_login_ip = useplayerloginip;
|
onMessageReceived.addListener(new Event.Listener<Message> () {
|
||||||
this.require_player_login_ip = requireplayerloginip;
|
|
||||||
this.check_user_ban = checkuserban;
|
|
||||||
onMessageReceived.addListener(new Listener<SendMessageHandler.Message>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void triggered(Message t) {
|
public void triggered(Message t) {
|
||||||
webChat(t.name, t.message);
|
webChat(t.name, t.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}};
|
}};
|
||||||
|
plugin.addServlet("/up/sendmessage", messageHandler);
|
||||||
plugin.webServer.handlers.put("/up/sendmessage", messageHandler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +48,7 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent {
|
|||||||
// TODO: Change null to something meaningful.
|
// TODO: Change null to something meaningful.
|
||||||
plugin.mapManager.pushUpdate(new Client.ChatMessage("web", null, name, message, null));
|
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);
|
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);
|
plugin.events.trigger("webchat", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<DynmapWorld>() {
|
||||||
|
@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.<JSONObject>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);
|
||||||
|
}
|
||||||
|
}
|
74
src/main/java/org/dynmap/servlet/ClientUpdateServlet.java
Normal file
74
src/main/java/org/dynmap/servlet/ClientUpdateServlet.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
578
src/main/java/org/dynmap/servlet/FileServlet.java
Normal file
578
src/main/java/org/dynmap/servlet/FileServlet.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Range> ranges = new ArrayList<Range>();
|
||||||
|
|
||||||
|
// 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<String, String> mimeTypes = new HashMap<String, String>() {{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
18
src/main/java/org/dynmap/servlet/JSONServlet.java
Normal file
18
src/main/java/org/dynmap/servlet/JSONServlet.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
146
src/main/java/org/dynmap/servlet/MainServlet.java
Normal file
146
src/main/java/org/dynmap/servlet/MainServlet.java
Normal file
@ -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<Registration> registrations = new LinkedList<Registration>();
|
||||||
|
public List<Header> customHeaders = new LinkedList<Header>();
|
||||||
|
|
||||||
|
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<String, Object> properties = new HashMap<String, Object>();
|
||||||
|
String path = req.getPathInfo();
|
||||||
|
|
||||||
|
for(Header header : customHeaders) {
|
||||||
|
resp.setHeader(header.name, header.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Registration bestMatch = null;
|
||||||
|
String bestMatchPart = null;
|
||||||
|
HashMap<String, Object> 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<String, Object>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.io.InputStreamReader;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -7,50 +21,40 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import org.bukkit.OfflinePlayer;
|
public class SendMessageServlet extends HttpServlet {
|
||||||
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 {
|
|
||||||
protected static final Logger log = Logger.getLogger("Minecraft");
|
protected static final Logger log = Logger.getLogger("Minecraft");
|
||||||
|
|
||||||
private static final JSONParser parser = new JSONParser();
|
private static final JSONParser parser = new JSONParser();
|
||||||
public Event<Message> onMessageReceived = new Event<SendMessageHandler.Message>();
|
public Event<Message> onMessageReceived = new Event<Message>();
|
||||||
private Charset cs_utf8 = Charset.forName("UTF-8");
|
private Charset cs_utf8 = Charset.forName("UTF-8");
|
||||||
public int maximumMessageInterval = 1000;
|
public int maximumMessageInterval = 1000;
|
||||||
public boolean hideip = false;
|
public boolean hideip = false;
|
||||||
public boolean trustclientname = 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.\"";
|
public String spamMessage = "\"You may only chat once every %interval% seconds.\"";
|
||||||
private HashMap<String, WebUser> disallowedUsers = new HashMap<String, WebUser>();
|
private HashMap<String, WebUser> disallowedUsers = new HashMap<String, WebUser>();
|
||||||
private LinkedList<WebUser> disallowedUserQueue = new LinkedList<WebUser>();
|
private LinkedList<WebUser> disallowedUserQueue = new LinkedList<WebUser>();
|
||||||
private Object disallowedUsersLock = new Object();
|
private Object disallowedUsersLock = new Object();
|
||||||
private HashMap<String,String> useralias = new HashMap<String,String>();
|
private HashMap<String,String> useralias = new HashMap<String,String>();
|
||||||
private int aliasindex = 1;
|
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
|
@Override
|
||||||
public void handle(String path, HttpRequest request, HttpResponse response) throws Exception {
|
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
if (!request.method.equals(HttpMethod.Post)) {
|
InputStreamReader reader = new InputStreamReader(request.getInputStream(), cs_utf8);
|
||||||
response.status = HttpStatus.MethodNotAllowed;
|
|
||||||
response.fields.put(HttpField.Accept, HttpMethod.Post);
|
JSONObject o = null;
|
||||||
|
try {
|
||||||
|
o = (JSONObject)parser.parse(reader);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
response.sendError(HttpStatus.BadRequest.getCode());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputStreamReader reader = new InputStreamReader(request.body, cs_utf8);
|
|
||||||
|
|
||||||
JSONObject o = (JSONObject)parser.parse(reader);
|
|
||||||
final Message message = new Message();
|
final Message message = new Message();
|
||||||
|
|
||||||
message.name = "";
|
message.name = "";
|
||||||
@ -59,44 +63,39 @@ public class SendMessageHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
boolean isip = true;
|
boolean isip = true;
|
||||||
if((message.name == null) || message.name.equals("")) {
|
if((message.name == null) || message.name.equals("")) {
|
||||||
/* If proxied client address, get original */
|
/* If proxied client address, get original */
|
||||||
if(request.fields.containsKey("X-Forwarded-For"))
|
if(request.getHeader("X-Forwarded-For") != null)
|
||||||
message.name = request.fields.get("X-Forwarded-For");
|
message.name = request.getHeader("X-Forwarded-For");
|
||||||
/* If from loopback, we're probably getting from proxy - need to trust client */
|
/* If from loopback, we're probably getting from proxy - need to trust client */
|
||||||
else if(request.rmtaddr.getAddress().isLoopbackAddress())
|
else if(request.getRemoteAddr() == "127.0.0.1")
|
||||||
message.name = String.valueOf(o.get("name"));
|
message.name = String.valueOf(o.get("name"));
|
||||||
else
|
else
|
||||||
message.name = request.rmtaddr.getAddress().getHostAddress();
|
message.name = request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
if(use_player_login_ip) {
|
if (use_player_login_ip) {
|
||||||
List<String> ids = plug_in.getIDsForIP(message.name);
|
List<String> ids = plug_in.getIDsForIP(message.name);
|
||||||
if(ids != null) {
|
if (ids != null) {
|
||||||
String id = ids.get(0);
|
String id = ids.get(0);
|
||||||
if(check_user_ban) {
|
if (check_user_ban) {
|
||||||
OfflinePlayer p = plug_in.getServer().getOfflinePlayer(id);
|
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 + ")");
|
Log.info("Ignore message from '" + message.name + "' - banned player (" + id + ")");
|
||||||
response.fields.put("Content-Length", "0");
|
response.sendError(HttpStatus.Forbidden.getCode());
|
||||||
response.status = HttpStatus.Forbidden;
|
|
||||||
response.getBody();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.name = ids.get(0);
|
message.name = ids.get(0);
|
||||||
isip = false;
|
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");
|
Log.info("Ignore message from '" + message.name + "' - no matching player login recorded");
|
||||||
response.fields.put("Content-Length", "0");
|
response.sendError(HttpStatus.Forbidden.getCode());
|
||||||
response.status = HttpStatus.Forbidden;
|
|
||||||
response.getBody();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(hideip && isip) { /* If hiding IP, find or assign alias */
|
if (hideip && isip) { /* If hiding IP, find or assign alias */
|
||||||
synchronized(disallowedUsersLock) {
|
synchronized (disallowedUsersLock) {
|
||||||
String n = useralias.get(message.name);
|
String n = useralias.get(message.name);
|
||||||
if(n == null) { /* Make ID */
|
if (n == null) { /* Make ID */
|
||||||
n = String.format("web-%03d", aliasindex);
|
n = String.format("web-%03d", aliasindex);
|
||||||
aliasindex++;
|
aliasindex++;
|
||||||
useralias.put(message.name, n);
|
useralias.put(message.name, n);
|
||||||
@ -108,7 +107,7 @@ public class SendMessageHandler implements HttpHandler {
|
|||||||
|
|
||||||
final long now = System.currentTimeMillis();
|
final long now = System.currentTimeMillis();
|
||||||
|
|
||||||
synchronized(disallowedUsersLock) {
|
synchronized (disallowedUsersLock) {
|
||||||
// Allow users that user that are now allowed to send messages.
|
// Allow users that user that are now allowed to send messages.
|
||||||
while (!disallowedUserQueue.isEmpty()) {
|
while (!disallowedUserQueue.isEmpty()) {
|
||||||
WebUser wu = disallowedUserQueue.getFirst();
|
WebUser wu = disallowedUserQueue.getFirst();
|
||||||
@ -122,25 +121,21 @@ public class SendMessageHandler implements HttpHandler {
|
|||||||
|
|
||||||
WebUser user = disallowedUsers.get(message.name);
|
WebUser user = disallowedUsers.get(message.name);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
user = new WebUser() {{
|
user = new WebUser() {
|
||||||
name = message.name;
|
{
|
||||||
nextMessageTime = now+maximumMessageInterval;
|
name = message.name;
|
||||||
}};
|
nextMessageTime = now + maximumMessageInterval;
|
||||||
|
}
|
||||||
|
};
|
||||||
disallowedUsers.put(user.name, user);
|
disallowedUsers.put(user.name, user);
|
||||||
disallowedUserQueue.add(user);
|
disallowedUserQueue.add(user);
|
||||||
} else {
|
} else {
|
||||||
response.fields.put("Content-Length", "0");
|
response.sendError(HttpStatus.Forbidden.getCode());
|
||||||
response.status = HttpStatus.Forbidden;
|
|
||||||
response.getBody();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageReceived.trigger(message);
|
onMessageReceived.trigger(message);
|
||||||
|
|
||||||
response.fields.put(HttpField.ContentLength, "0");
|
|
||||||
response.status = HttpStatus.OK;
|
|
||||||
response.getBody();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Message {
|
public static class Message {
|
@ -1,6 +0,0 @@
|
|||||||
package org.dynmap.web;
|
|
||||||
|
|
||||||
|
|
||||||
public interface HttpHandler {
|
|
||||||
void handle(String path, HttpRequest request, HttpResponse response) throws Exception;
|
|
||||||
}
|
|
@ -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<String, String> fields = new HashMap<String, String>();
|
|
||||||
public InputStream body;
|
|
||||||
public InetSocketAddress rmtaddr;
|
|
||||||
}
|
|
@ -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<String, String> fields = new HashMap<String, String>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<String, HttpHandler> handlers = new TreeMap<String, HttpHandler>(Collections.reverseOrder());
|
|
||||||
|
|
||||||
private Object lock = new Object();
|
|
||||||
private HashSet<HttpServerConnection> active_connections = new HashSet<HttpServerConnection>();
|
|
||||||
private HashSet<HttpServerConnection> keepalive_connections = new HashSet<HttpServerConnection>();
|
|
||||||
private Plugin plugin;
|
|
||||||
private static Map<String, String> headers = new HashMap<String,String>();
|
|
||||||
|
|
||||||
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<HttpServerConnection> sc;
|
|
||||||
synchronized(lock) {
|
|
||||||
sc = new HashSet<HttpServerConnection>(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<String> banned_ips = new HashSet<String>();
|
|
||||||
private HashSet<String> banned_ips_notified = new HashSet<String>();
|
|
||||||
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<String,String> getCustomHeaders() {
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
public static void setCustomHeaders(Map<String,String> hdrs) {
|
|
||||||
headers = hdrs;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<String, String> field : response.fields.entrySet()) {
|
|
||||||
out.append(field.getKey());
|
|
||||||
out.append(": ");
|
|
||||||
out.append(field.getValue());
|
|
||||||
out.append("\r\n");
|
|
||||||
}
|
|
||||||
for(Entry<String, String> 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<String, HttpHandler> 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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<DynmapWorld>() {
|
|
||||||
@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.<JSONObject>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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<byte[]> bufferpool = new LinkedList<byte[]>();
|
|
||||||
private Object lock = new Object();
|
|
||||||
private static final int MAX_FREE_IN_POOL = 2;
|
|
||||||
|
|
||||||
private static Map<String, String> mimes = new HashMap<String, String>();
|
|
||||||
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) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user