By default, the server spawns a new Thread for every incoming request. These are set
* to daemon status, and named according to the request number. The name is
* useful when profiling the application.
@@ -367,76 +477,10 @@ public abstract class NanoHTTPD
t.start();
}
}
- // ------------------------------------------------------------------------------- //
- //
- // Temp file handling strategy.
- //
- // ------------------------------------------------------------------------------- //
- /**
- * Pluggable strategy for creating and cleaning up temporary files.
- */
- private TempFileManagerFactory tempFileManagerFactory;
-
- /**
- * Pluggable strategy for creating and cleaning up temporary files.
- * @param tempFileManagerFactory new strategy for handling temp files.
- */
- public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory)
- {
- this.tempFileManagerFactory = tempFileManagerFactory;
- }
-
- /**
- * Factory to create temp file managers.
- */
- public interface TempFileManagerFactory
- {
- TempFileManager create();
- }
-
- /**
- * Temp file manager.
- *
- *
Temp file managers are created 1-to-1 with incoming requests, to create and cleanup
- * temporary files created as a result of handling the request.
Temp files are responsible for managing the actual temporary storage and cleaning
- * themselves up when no longer needed.
- */
- public interface TempFile
- {
- OutputStream open() throws Exception;
-
- void delete() throws Exception;
-
- String getName();
- }
/**
* Default strategy for creating and cleaning up temporary files.
- */
- private class DefaultTempFileManagerFactory implements TempFileManagerFactory
- {
- @Override
- public TempFileManager create()
- {
- return new DefaultTempFileManager();
- }
- }
-
- /**
- * Default strategy for creating and cleaning up temporary files.
- *
+ *
* This class stores its files in the standard location (that is,
* wherever java.io.tmpdir points to). Files are added
* to an internal list, and deleted when no longer needed (that is,
@@ -481,7 +525,7 @@ public abstract class NanoHTTPD
/**
* Default strategy for creating and cleaning up temporary files.
- *
+ *
* [>By default, files are created by File.createTempFile() in
* the directory specified.
*/
@@ -516,7 +560,6 @@ public abstract class NanoHTTPD
}
}
- // ------------------------------------------------------------------------------- //
/**
* HTTP response. Return one of these from serve().
*/
@@ -542,6 +585,10 @@ public abstract class NanoHTTPD
* The request method that spawned this response.
*/
private Method requestMethod;
+ /**
+ * Use chunkedTransfer
+ */
+ private boolean chunkedTransfer;
/**
* Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
@@ -623,31 +670,15 @@ public abstract class NanoHTTPD
}
}
- int pending = data != null ? data.available() : -1; // This is to support partial sends, see serveFile()
- if (pending > 0)
+ pw.print("Connection: keep-alive\r\n");
+
+ if (requestMethod != Method.HEAD && chunkedTransfer)
{
- pw.print("Connection: keep-alive\r\n");
- pw.print("Content-Length: " + pending + "\r\n");
+ sendAsChunked(outputStream, pw);
}
-
- pw.print("\r\n");
- pw.flush();
-
- if (requestMethod != Method.HEAD && data != null)
+ else
{
- int BUFFER_SIZE = 16 * 1024;
- byte[] buff = new byte[BUFFER_SIZE];
- while (pending > 0)
- {
- int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
- if (read <= 0)
- {
- break;
- }
- outputStream.write(buff, 0, read);
-
- pending -= read;
- }
+ sendAsFixedLength(outputStream, pw);
}
outputStream.flush();
safeClose(data);
@@ -658,6 +689,50 @@ public abstract class NanoHTTPD
}
}
+ private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException
+ {
+ pw.print("Transfer-Encoding: chunked\r\n");
+ pw.print("\r\n");
+ pw.flush();
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] CRLF = "\r\n".getBytes();
+ byte[] buff = new byte[BUFFER_SIZE];
+ int read;
+ while ((read = data.read(buff)) > 0)
+ {
+ outputStream.write(String.format("%x\r\n", read).getBytes());
+ outputStream.write(buff, 0, read);
+ outputStream.write(CRLF);
+ }
+ outputStream.write(String.format("0\r\n\r\n").getBytes());
+ }
+
+ private void sendAsFixedLength(OutputStream outputStream, PrintWriter pw) throws IOException
+ {
+ int pending = data != null ? data.available() : 0; // This is to support partial sends, see serveFile()
+ pw.print("Content-Length: " + pending + "\r\n");
+
+ pw.print("\r\n");
+ pw.flush();
+
+ if (requestMethod != Method.HEAD && data != null)
+ {
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] buff = new byte[BUFFER_SIZE];
+ while (pending > 0)
+ {
+ int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
+ if (read <= 0)
+ {
+ break;
+ }
+ outputStream.write(buff, 0, read);
+
+ pending -= read;
+ }
+ }
+ }
+
public Status getStatus()
{
return status;
@@ -698,6 +773,11 @@ public abstract class NanoHTTPD
this.requestMethod = requestMethod;
}
+ public void setChunkedTransfer(boolean chunkedTransfer)
+ {
+ this.chunkedTransfer = chunkedTransfer;
+ }
+
/**
* Some HTTP response status codes
*/
@@ -728,6 +808,40 @@ public abstract class NanoHTTPD
}
}
+ public static final class ResponseException extends Exception
+ {
+ private final Response.Status status;
+
+ public ResponseException(Response.Status status, String message)
+ {
+ super(message);
+ this.status = status;
+ }
+
+ public ResponseException(Response.Status status, String message, Exception e)
+ {
+ super(message, e);
+ this.status = status;
+ }
+
+ public Response.Status getStatus()
+ {
+ return status;
+ }
+ }
+
+ /**
+ * Default strategy for creating and cleaning up temporary files.
+ */
+ private class DefaultTempFileManagerFactory implements TempFileManagerFactory
+ {
+ @Override
+ public TempFileManager create()
+ {
+ return new DefaultTempFileManager();
+ }
+ }
+
/**
* Handles one session, i.e. parses the HTTP request and returns the response.
*/
@@ -735,17 +849,18 @@ public abstract class NanoHTTPD
{
public static final int BUFSIZE = 8192;
private final TempFileManager tempFileManager;
- private InputStream inputStream;
private final OutputStream outputStream;
+ private final Socket socket;
+ private InputStream inputStream;
private int splitbyte;
private int rlen;
private String uri;
private Method method;
private Map parms;
private Map headers;
- private Socket socket;
+ private CookieHandler cookies;
- private HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, Socket socket)
+ public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, Socket socket)
{
this.tempFileManager = tempFileManager;
this.inputStream = inputStream;
@@ -808,6 +923,8 @@ public abstract class NanoHTTPD
uri = pre.get("uri");
+ cookies = new CookieHandler(headers);
+
// Ok, now do the serve()
Response r = serve(this);
if (r == null)
@@ -816,6 +933,7 @@ public abstract class NanoHTTPD
}
else
{
+ cookies.unloadQueue(r);
r.setRequestMethod(method);
r.send(outputStream);
}
@@ -843,7 +961,7 @@ public abstract class NanoHTTPD
}
}
- private void parseBody(Map files) throws IOException, ResponseException
+ protected void parseBody(Map files) throws IOException, ResponseException
{
RandomAccessFile randomAccessFile = null;
BufferedReader in = null;
@@ -914,7 +1032,7 @@ public abstract class NanoHTTPD
String boundaryStartString = "boundary=";
int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
- if (boundary.startsWith("\"") && boundary.startsWith("\""))
+ if (boundary.startsWith("\"") && boundary.endsWith("\""))
{
boundary = boundary.substring(1, boundary.length() - 1);
}
@@ -1189,7 +1307,7 @@ public abstract class NanoHTTPD
path = tempFile.getName();
}
catch (Exception e)
- {
+ { // Catch exception if any
TFM_Log.severe(e);
}
finally
@@ -1285,88 +1403,140 @@ public abstract class NanoHTTPD
return inputStream;
}
- public final Socket getSocket()
+ public CookieHandler getCookies()
+ {
+ return cookies;
+ }
+
+ public Socket getSocket()
{
return socket;
}
}
- private static final class ResponseException extends Exception
+ public static class Cookie
{
- private final Response.Status status;
+ private String n, v, e;
- public ResponseException(Response.Status status, String message)
+ public Cookie(String name, String value, String expires)
{
- super(message);
- this.status = status;
+ n = name;
+ v = value;
+ e = expires;
}
- public ResponseException(Response.Status status, String message, Exception e)
+ public Cookie(String name, String value)
{
- super(message, e);
- this.status = status;
+ this(name, value, 30);
}
- public Response.Status getStatus()
+ public Cookie(String name, String value, int numDays)
{
- return status;
+ n = name;
+ v = value;
+ e = getHTTPTime(numDays);
+ }
+
+ public String getHTTPHeader()
+ {
+ String fmt = "%s=%s; expires=%s";
+ return String.format(fmt, n, v, e);
+ }
+
+ public static String getHTTPTime(int days)
+ {
+ Calendar calendar = Calendar.getInstance();
+ SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ calendar.add(Calendar.DAY_OF_MONTH, days);
+ return dateFormat.format(calendar.getTime());
}
}
- private static final void safeClose(ServerSocket serverSocket)
+ /**
+ * Provides rudimentary support for cookies.
+ * Doesn't support 'path', 'secure' nor 'httpOnly'.
+ * Feel free to improve it and/or add unsupported features.
+ *
+ * @author LordFokas
+ */
+ public class CookieHandler implements Iterable
{
- if (serverSocket != null)
+ private HashMap cookies = new HashMap();
+ private ArrayList queue = new ArrayList();
+
+ public CookieHandler(Map httpHeaders)
{
- try
+ String raw = httpHeaders.get("cookie");
+ if (raw != null)
{
- serverSocket.close();
+ String[] tokens = raw.split(";");
+ for (String token : tokens)
+ {
+ String[] data = token.trim().split("=");
+ if (data.length == 2)
+ {
+ cookies.put(data[0], data[1]);
+ }
+ }
}
- catch (IOException e)
+ }
+
+ @Override
+ public Iterator iterator()
+ {
+ return cookies.keySet().iterator();
+ }
+
+ /**
+ * Read a cookie from the HTTP Headers.
+ *
+ * @param name The cookie's name.
+ * @return The cookie's value if it exists, null otherwise.
+ */
+ public String read(String name)
+ {
+ return cookies.get(name);
+ }
+
+ /**
+ * Sets a cookie.
+ *
+ * @param name The cookie's name.
+ * @param value The cookie's value.
+ * @param expires How many days until the cookie expires.
+ */
+ public void set(String name, String value, int expires)
+ {
+ queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
+ }
+
+ public void set(Cookie cookie)
+ {
+ queue.add(cookie);
+ }
+
+ /**
+ * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side.
+ *
+ * @param name The cookie name.
+ */
+ public void delete(String name)
+ {
+ set(name, "-delete-", -30);
+ }
+
+ /**
+ * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers.
+ *
+ * @param response The Response object to which headers the queued cookies will be added.
+ */
+ public void unloadQueue(Response response)
+ {
+ for (Cookie cookie : queue)
{
+ response.addHeader("Set-Cookie", cookie.getHTTPHeader());
}
}
}
-
- private static final void safeClose(Socket socket)
- {
- if (socket != null)
- {
- try
- {
- socket.close();
- }
- catch (IOException e)
- {
- }
- }
- }
-
- private static final void safeClose(Closeable closeable)
- {
- if (closeable != null)
- {
- try
- {
- closeable.close();
- }
- catch (IOException e)
- {
- }
- }
- }
-
- public final int getListeningPort()
- {
- return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
- }
-
- public final boolean wasStarted()
- {
- return myServerSocket != null && myThread != null;
- }
-
- public final boolean isAlive()
- {
- return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive();
- }
-}
\ No newline at end of file
+}
diff --git a/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Manager.java b/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Manager.java
index 58826045..8966d573 100644
--- a/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Manager.java
+++ b/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Manager.java
@@ -3,12 +3,10 @@ package me.StevenLawson.TotalFreedomMod.HTTPD;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
-import java.net.Socket;
-import java.util.Map;
import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import me.StevenLawson.TotalFreedomMod.HTTPD.NanoHTTPD.HTTPSession;
import me.StevenLawson.TotalFreedomMod.HTTPD.NanoHTTPD.Response;
import me.StevenLawson.TotalFreedomMod.TFM_ConfigEntry;
import me.StevenLawson.TotalFreedomMod.TFM_Log;
@@ -19,6 +17,9 @@ import org.bukkit.Bukkit;
public class TFM_HTTPD_Manager
{
+ @Deprecated
+ public static String MIME_DEFAULT_BINARY = "application/octet-stream";
+ //
private static final Pattern EXT_REGEX = Pattern.compile("\\.([^\\.\\s]+)$");
//
public static final int PORT = TFM_ConfigEntry.HTTPD_PORT.getInteger();
@@ -69,36 +70,118 @@ public class TFM_HTTPD_Manager
private static enum ModuleType
{
- DUMP(false, "dump"),
- HELP(true, "help"),
- LIST(true, "list"),
- FILE(false, "file"),
- SCHEMATIC(false, "schematic"),
- PERMBANS(false, "permbans");
- private final boolean runOnBukkitThread;
- private final String name;
-
- private ModuleType(boolean runOnBukkitThread, String name)
+ DUMP(new ModuleExecutable(false, "dump")
{
- this.runOnBukkitThread = runOnBukkitThread;
- this.name = name;
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Response(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, "The DUMP module is disabled. It is intended for debugging use only.");
+ }
+ }),
+ HELP(new ModuleExecutable(true, "help")
+ {
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Module_help(session).getResponse();
+ }
+ }),
+ LIST(new ModuleExecutable(true, "list")
+ {
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Module_list(session).getResponse();
+ }
+ }),
+ FILE(new ModuleExecutable(false, "file")
+ {
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Module_file(session).getResponse();
+ }
+ }),
+ SCHEMATIC(new ModuleExecutable(false, "schematic")
+ {
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Module_schematic(session).getResponse();
+ }
+ }),
+ PERMBANS(new ModuleExecutable(false, "permbans")
+ {
+ @Override
+ public Response getResponse(HTTPSession session)
+ {
+ return new Module_permbans(session).getResponse();
+ }
+ });
+ //
+ private final ModuleExecutable moduleExecutable;
+
+ private ModuleType(ModuleExecutable moduleExecutable)
+ {
+ this.moduleExecutable = moduleExecutable;
}
- public boolean isRunOnBukkitThread()
+ private abstract static class ModuleExecutable
{
- return runOnBukkitThread;
+ private final boolean runOnBukkitThread;
+ private final String name;
+
+ public ModuleExecutable(boolean runOnBukkitThread, String name)
+ {
+ this.runOnBukkitThread = runOnBukkitThread;
+ this.name = name;
+ }
+
+ public Response execute(final HTTPSession session)
+ {
+ try
+ {
+ if (this.runOnBukkitThread)
+ {
+ return Bukkit.getScheduler().callSyncMethod(TotalFreedomMod.plugin, new Callable()
+ {
+ @Override
+ public Response call() throws Exception
+ {
+ return getResponse(session);
+ }
+ }).get();
+ }
+ else
+ {
+ return getResponse(session);
+ }
+ }
+ catch (Exception ex)
+ {
+ TFM_Log.severe(ex);
+ }
+ return null;
+ }
+
+ public abstract Response getResponse(HTTPSession session);
+
+ public String getName()
+ {
+ return name;
+ }
}
- public String getName()
+ public ModuleExecutable getModuleExecutable()
{
- return name;
+ return moduleExecutable;
}
private static ModuleType getByName(String needle)
{
for (ModuleType type : values())
{
- if (type.getName().equalsIgnoreCase(needle))
+ if (type.getModuleExecutable().getName().equalsIgnoreCase(needle))
{
return type;
}
@@ -120,68 +203,15 @@ public class TFM_HTTPD_Manager
}
@Override
- public Response serve(
- final String uri,
- final Method method,
- final Map headers,
- final Map params,
- final Map files,
- final Socket socket)
+ public Response serve(HTTPSession session)
{
- Response response = null;
+ Response response;
try
{
-
- final String[] args = StringUtils.split(uri, "/");
+ final String[] args = StringUtils.split(session.getUri(), "/");
final ModuleType moduleType = args.length >= 1 ? ModuleType.getByName(args[0]) : ModuleType.FILE;
-
- if (moduleType.isRunOnBukkitThread())
- {
- Future responseCall = Bukkit.getScheduler().callSyncMethod(TotalFreedomMod.plugin, new Callable()
- {
- @Override
- public Response call() throws Exception
- {
- switch (moduleType)
- {
- case HELP:
- return new Module_help(uri, method, headers, params, files, socket).getResponse();
- case LIST:
- return new Module_list(uri, method, headers, params, files, socket).getResponse();
- default:
- return null;
- }
- }
- });
-
- try
- {
- response = responseCall.get();
- }
- catch (Exception ex)
- {
- TFM_Log.severe(ex);
- }
- }
- else
- {
- switch (moduleType)
- {
- case DUMP:
- //response = new Module_dump(uri, method, headers, params, files, socket).getResponse();
- response = new Response(Response.Status.OK, MIME_PLAINTEXT, "The DUMP module is disabled. It is intended for debugging use only.");
- break;
- case SCHEMATIC:
- response = new Module_schematic(uri, method, headers, params, files, socket).getResponse();
- break;
- case PERMBANS:
- response = new Module_permbans(uri, method, headers, params, files, socket).getResponse();
- break;
- default:
- response = new Module_file(uri, method, headers, params, files, socket).getResponse();
- }
- }
+ response = moduleType.getModuleExecutable().execute(session);
}
catch (Exception ex)
{
@@ -215,10 +245,10 @@ public class TFM_HTTPD_Manager
if (mimetype == null || mimetype.trim().isEmpty())
{
- mimetype = NanoHTTPD.MIME_DEFAULT_BINARY;
+ mimetype = MIME_DEFAULT_BINARY;
}
- response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, mimetype, new FileInputStream(file));
+ response = new Response(Response.Status.OK, mimetype, new FileInputStream(file));
response.addHeader("Content-Length", "" + file.length());
}
catch (IOException ex)
diff --git a/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Module.java b/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Module.java
index a6aa4b0d..98688d7a 100644
--- a/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Module.java
+++ b/src/me/StevenLawson/TotalFreedomMod/HTTPD/TFM_HTTPD_Module.java
@@ -1,8 +1,10 @@
package me.StevenLawson.TotalFreedomMod.HTTPD;
import java.net.Socket;
+import java.util.HashMap;
import java.util.Map;
import me.StevenLawson.TotalFreedomMod.HTTPD.NanoHTTPD.*;
+import me.StevenLawson.TotalFreedomMod.TFM_Log;
public abstract class TFM_HTTPD_Module
{
@@ -10,17 +12,17 @@ public abstract class TFM_HTTPD_Module
protected final Method method;
protected final Map headers;
protected final Map params;
- protected final Map files;
protected final Socket socket;
+ protected final HTTPSession session;
- public TFM_HTTPD_Module(String uri, Method method, Map headers, Map params, Map files, Socket socket)
+ public TFM_HTTPD_Module(HTTPSession session)
{
- this.uri = uri;
- this.method = method;
- this.headers = headers;
- this.params = params;
- this.files = files;
- this.socket = socket;
+ this.uri = session.getUri();
+ this.method = session.getMethod();
+ this.headers = session.getHeaders();
+ this.params = session.getParms();
+ this.socket = session.getSocket();
+ this.session = session;
}
public String getBody()
@@ -47,4 +49,20 @@ public abstract class TFM_HTTPD_Module
{
return new TFM_HTTPD_PageBuilder(getBody(), getTitle(), getStyle(), getScript()).getResponse();
}
+
+ protected final Map getFiles()
+ {
+ Map files = new HashMap();
+
+ try
+ {
+ session.parseBody(files);
+ }
+ catch (Exception ex)
+ {
+ TFM_Log.severe(ex);
+ }
+
+ return files;
+ }
}