aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/src/main/java/fi/iki/elonen/NanoHTTPD.java2307
-rw-r--r--markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java21
-rw-r--r--markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java8
-rw-r--r--webserver/src/main/java/fi/iki/elonen/InternalRewrite.java8
-rw-r--r--webserver/src/main/java/fi/iki/elonen/ServerRunner.java16
-rw-r--r--webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java363
-rw-r--r--webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java4
-rw-r--r--webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java4
-rw-r--r--websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java1028
-rw-r--r--websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java40
10 files changed, 1906 insertions, 1893 deletions
diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
index 0705196..06afeb9 100644
--- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
+++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -56,6 +56,7 @@ import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
+import java.security.KeyStore;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
@@ -72,8 +73,12 @@ import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.security.KeyStore;
-import javax.net.ssl.*;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
/**
* A simple, tiny, nicely embeddable HTTP server in Java
@@ -129,499 +134,134 @@ import javax.net.ssl.*;
public abstract class NanoHTTPD {
/**
- * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
- * This is required as the Keep-Alive HTTP connections would otherwise block
- * the socket reading thread forever (or as long the browser is open).
- */
- public static final int SOCKET_READ_TIMEOUT = 5000;
-
- /**
- * Common MIME type for dynamic content: plain text
- */
- public static final String MIME_PLAINTEXT = "text/plain";
-
- /**
- * Common MIME type for dynamic content: html
- */
- public static final String MIME_HTML = "text/html";
-
- /**
- * Pseudo-Parameter to use to store the actual query string in the
- * parameters map for later re-processing.
- */
- private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
-
- /**
- * logger to log to.
- */
- private static Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
-
- private final String hostname;
-
- private final int myPort;
-
- private ServerSocket myServerSocket;
-
- private Set<Socket> openConnections = new HashSet<Socket>();
-
- private SSLServerSocketFactory sslServerSocketFactory;
-
- private Thread myThread;
-
- /**
* Pluggable strategy for asynchronously executing requests.
*/
- private AsyncRunner asyncRunner;
-
- /**
- * Pluggable strategy for creating and cleaning up temporary files.
- */
- private TempFileManagerFactory tempFileManagerFactory;
+ public interface AsyncRunner {
- /**
- * Constructs an HTTP server on given port.
- */
- public NanoHTTPD(int port) {
- this(null, port);
+ void exec(Runnable code);
}
- /**
- * Constructs an HTTP server on given hostname and port.
- */
- public NanoHTTPD(String hostname, int port) {
- this.hostname = hostname;
- this.myPort = port;
- setTempFileManagerFactory(new DefaultTempFileManagerFactory());
- setAsyncRunner(new DefaultAsyncRunner());
- }
+ public static class Cookie {
- private static final void safeClose(Closeable closeable) {
- if (closeable != null) {
- try {
- closeable.close();
- } catch (IOException e) {
- LOG.log(Level.SEVERE, "Could not close", 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());
}
- }
- /**
- * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
- * certificate and passphrase
- */
- public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
- SSLServerSocketFactory res = null;
- try {
- KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
- InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
- keystore.load(keystoreStream, passphrase);
- TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init(keystore);
- KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- keyManagerFactory.init(keystore, passphrase);
- SSLContext ctx = SSLContext.getInstance("TLS");
- ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
- res = ctx.getServerSocketFactory();
- } catch (Exception e) {
- throw new IOException(e.getMessage());
+ private final String n, v, e;
+
+ public Cookie(String name, String value) {
+ this(name, value, 30);
}
- return res;
- }
- /**
- * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
- * loaded KeyManagerFactory. These objects must properly loaded/initialized
- * by the caller.
- */
- public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
- SSLServerSocketFactory res = null;
- try {
- TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init(loadedKeyStore);
- SSLContext ctx = SSLContext.getInstance("TLS");
- ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
- res = ctx.getServerSocketFactory();
- } catch (Exception e) {
- throw new IOException(e.getMessage());
+ public Cookie(String name, String value, int numDays) {
+ this.n = name;
+ this.v = value;
+ this.e = getHTTPTime(numDays);
}
- return res;
- }
- /**
- * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
- * array of loaded KeyManagers. These objects must properly
- * loaded/initialized by the caller.
- */
- public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
- SSLServerSocketFactory res = null;
- try {
- TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init(loadedKeyStore);
- SSLContext ctx = SSLContext.getInstance("TLS");
- ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
- res = ctx.getServerSocketFactory();
- } catch (Exception e) {
- throw new IOException(e.getMessage());
+ public Cookie(String name, String value, String expires) {
+ this.n = name;
+ this.v = value;
+ this.e = expires;
}
- return res;
- }
- /**
- * Call before start() to serve over HTTPS instead of HTTP
- */
- public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) {
- this.sslServerSocketFactory = sslServerSocketFactory;
+ public String getHTTPHeader() {
+ String fmt = "%s=%s; expires=%s";
+ return String.format(fmt, this.n, this.v, this.e);
+ }
}
/**
- * Start the server.
+ * Provides rudimentary support for cookies. Doesn't support 'path',
+ * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported
+ * features.
*
- * @throws IOException
- * if the socket is in use.
+ * @author LordFokas
*/
- public void start() throws IOException {
- if (sslServerSocketFactory != null) {
- SSLServerSocket ss = (SSLServerSocket) sslServerSocketFactory.createServerSocket();
- ss.setNeedClientAuth(false);
- myServerSocket = ss;
- } else {
- myServerSocket = new ServerSocket();
- }
- myServerSocket.setReuseAddress(true);
- myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
+ public class CookieHandler implements Iterable<String> {
- myThread = new Thread(new Runnable() {
+ private final HashMap<String, String> cookies = new HashMap<String, String>();
- @Override
- public void run() {
- do {
- try {
- final Socket finalAccept = myServerSocket.accept();
- registerConnection(finalAccept);
- finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
- final InputStream inputStream = finalAccept.getInputStream();
- asyncRunner.exec(new Runnable() {
+ private final ArrayList<Cookie> queue = new ArrayList<Cookie>();
- @Override
- public void run() {
- OutputStream outputStream = null;
- try {
- outputStream = finalAccept.getOutputStream();
- TempFileManager tempFileManager = tempFileManagerFactory.create();
- HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
- while (!finalAccept.isClosed()) {
- session.execute();
- }
- } catch (Exception e) {
- // When the socket is closed by the client,
- // we throw our own SocketException
- // to break the "keep alive" loop above. If
- // the exception was anything other
- // than the expected SocketException OR a
- // SocketTimeoutException, print the
- // stacktrace
- if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
- LOG.log(Level.FINE, "Communication with the client broken", e);
- }
- } finally {
- safeClose(outputStream);
- safeClose(inputStream);
- safeClose(finalAccept);
- unRegisterConnection(finalAccept);
- }
- }
- });
- } catch (IOException e) {
- LOG.log(Level.FINE, "Communication with the client broken", e);
+ public CookieHandler(Map<String, String> httpHeaders) {
+ String raw = httpHeaders.get("cookie");
+ if (raw != null) {
+ String[] tokens = raw.split(";");
+ for (String token : tokens) {
+ String[] data = token.trim().split("=");
+ if (data.length == 2) {
+ this.cookies.put(data[0], data[1]);
}
- } while (!myServerSocket.isClosed());
- }
- });
- myThread.setDaemon(true);
- myThread.setName("NanoHttpd Main Listener");
- myThread.start();
- }
-
- /**
- * Stop the server.
- */
- public void stop() {
- try {
- safeClose(myServerSocket);
- closeAllConnections();
- if (myThread != null) {
- myThread.join();
+ }
}
- } catch (Exception e) {
- LOG.log(Level.SEVERE, "Could not stop all connections", e);
}
- }
- /**
- * Registers that a new connection has been set up.
- *
- * @param socket
- * the {@link Socket} for the connection.
- */
- public synchronized void registerConnection(Socket socket) {
- openConnections.add(socket);
- }
-
- /**
- * Registers that a connection has been closed
- *
- * @param socket
- * the {@link Socket} for the connection.
- */
- public synchronized void unRegisterConnection(Socket socket) {
- openConnections.remove(socket);
- }
-
- /**
- * Forcibly closes all connections that are open.
- */
- public synchronized void closeAllConnections() {
- for (Socket socket : openConnections) {
- safeClose(socket);
+ /**
+ * 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);
}
- }
-
- 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();
- }
-
- /**
- * Override this to customize the server.
- * <p/>
- * <p/>
- * (By default, this returns a 404 "Not Found" plain text error response.)
- *
- * @param uri
- * Percent-decoded URI without parameters, for example
- * "/index.cgi"
- * @param method
- * "GET", "POST" etc.
- * @param parms
- * Parsed, percent decoded parameters from URI and, in case of
- * POST, data.
- * @param headers
- * Header entries, percent decoded
- * @return HTTP response, see class Response for details
- */
- @Deprecated
- public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
- return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
- }
-
- /**
- * Override this to customize the server.
- * <p/>
- * <p/>
- * (By default, this returns a 404 "Not Found" plain text error response.)
- *
- * @param session
- * The HTTP session
- * @return HTTP response, see class Response for details
- */
- public Response serve(IHTTPSession session) {
- Map<String, String> files = new HashMap<String, String>();
- Method method = session.getMethod();
- if (Method.PUT.equals(method) || Method.POST.equals(method)) {
- try {
- session.parseBody(files);
- } catch (IOException ioe) {
- return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
- } catch (ResponseException re) {
- return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
- }
+ @Override
+ public Iterator<String> iterator() {
+ return this.cookies.keySet().iterator();
}
- Map<String, String> parms = session.getParms();
- parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
- return serve(session.getUri(), method, session.getHeaders(), parms, files);
- }
-
- /**
- * Decode percent encoded <code>String</code> values.
- *
- * @param str
- * the percent encoded <code>String</code>
- * @return expanded form of the input, for example "foo%20bar" becomes
- * "foo bar"
- */
- protected String decodePercent(String str) {
- String decoded = null;
- try {
- decoded = URLDecoder.decode(str, "UTF8");
- } catch (UnsupportedEncodingException ignored) {
- LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
+ /**
+ * 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 this.cookies.get(name);
}
- return decoded;
- }
- /**
- * Decode parameters from a URL, handing the case where a single parameter
- * name might have been supplied several times, by return lists of values.
- * In general these lists will contain a single element.
- *
- * @param parms
- * original <b>NanoHTTPD</b> parameters values, as passed to the
- * <code>serve()</code> method.
- * @return a map of <code>String</code> (parameter name) to
- * <code>List&lt;String&gt;</code> (a list of the values supplied).
- */
- protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
- return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
- }
-
- /**
- * Decode parameters from a URL, handing the case where a single parameter
- * name might have been supplied several times, by return lists of values.
- * In general these lists will contain a single element.
- *
- * @param queryString
- * a query string pulled from the URL.
- * @return a map of <code>String</code> (parameter name) to
- * <code>List&lt;String&gt;</code> (a list of the values supplied).
- */
- protected Map<String, List<String>> decodeParameters(String queryString) {
- Map<String, List<String>> parms = new HashMap<String, List<String>>();
- if (queryString != null) {
- StringTokenizer st = new StringTokenizer(queryString, "&");
- while (st.hasMoreTokens()) {
- String e = st.nextToken();
- int sep = e.indexOf('=');
- String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
- if (!parms.containsKey(propertyName)) {
- parms.put(propertyName, new ArrayList<String>());
- }
- String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
- if (propertyValue != null) {
- parms.get(propertyName).add(propertyValue);
- }
- }
+ public void set(Cookie cookie) {
+ this.queue.add(cookie);
}
- return parms;
- }
-
- // -------------------------------------------------------------------------------
- // //
- //
- // Threading Strategy.
- //
- // -------------------------------------------------------------------------------
- // //
-
- /**
- * Pluggable strategy for asynchronously executing requests.
- *
- * @param asyncRunner
- * new strategy for handling threads.
- */
- public void setAsyncRunner(AsyncRunner asyncRunner) {
- this.asyncRunner = asyncRunner;
- }
-
- // -------------------------------------------------------------------------------
- // //
- //
- // Temp file handling strategy.
- //
- // -------------------------------------------------------------------------------
- // //
-
- /**
- * 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;
- }
- /**
- * HTTP Request methods, with the ability to decode a <code>String</code>
- * back to its enum value.
- */
- public enum Method {
- GET,
- PUT,
- POST,
- DELETE,
- HEAD,
- OPTIONS;
+ /**
+ * 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) {
+ this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
+ }
- static Method lookup(String method) {
- for (Method m : Method.values()) {
- if (m.toString().equalsIgnoreCase(method)) {
- return m;
- }
+ /**
+ * 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 : this.queue) {
+ response.addHeader("Set-Cookie", cookie.getHTTPHeader());
}
- return null;
}
}
/**
- * Pluggable strategy for asynchronously executing requests.
- */
- public interface AsyncRunner {
-
- void exec(Runnable code);
- }
-
- /**
- * Factory to create temp file managers.
- */
- public interface TempFileManagerFactory {
-
- TempFileManager create();
- }
-
- // -------------------------------------------------------------------------------
- // //
-
- /**
- * Temp file manager.
- * <p/>
- * <p>
- * 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.
- * </p>
- */
- public interface TempFileManager {
-
- TempFile createTempFile() throws Exception;
-
- void clear();
- }
-
- /**
- * A temp file.
- * <p/>
- * <p>
- * Temp files are responsible for managing the actual temporary storage and
- * cleaning themselves up when no longer needed.
- * </p>
- */
- public interface TempFile {
-
- OutputStream open() throws Exception;
-
- void delete() throws Exception;
-
- String getName();
- }
-
- /**
* Default threading strategy for NanoHTTPD.
* <p/>
* <p>
@@ -636,10 +276,10 @@ public abstract class NanoHTTPD {
@Override
public void exec(Runnable code) {
- ++requestCount;
+ ++this.requestCount;
Thread t = new Thread(code);
t.setDaemon(true);
- t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
+ t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
t.start();
}
}
@@ -648,365 +288,76 @@ public abstract class NanoHTTPD {
* Default strategy for creating and cleaning up temporary files.
* <p/>
* <p>
- * This class stores its files in the standard location (that is, wherever
- * <code>java.io.tmpdir</code> points to). Files are added to an internal
- * list, and deleted when no longer needed (that is, when
- * <code>clear()</code> is invoked at the end of processing a request).
- * </p>
- */
- public static class DefaultTempFileManager implements TempFileManager {
-
- private final String tmpdir;
-
- private final List<TempFile> tempFiles;
-
- public DefaultTempFileManager() {
- tmpdir = System.getProperty("java.io.tmpdir");
- tempFiles = new ArrayList<TempFile>();
- }
-
- @Override
- public TempFile createTempFile() throws Exception {
- DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
- tempFiles.add(tempFile);
- return tempFile;
- }
-
- @Override
- public void clear() {
- for (TempFile file : tempFiles) {
- try {
- file.delete();
- } catch (Exception ignored) {
- LOG.log(Level.WARNING, "could not delete file ", ignored);
- }
- }
- tempFiles.clear();
- }
- }
-
- /**
- * Default strategy for creating and cleaning up temporary files.
- * <p/>
- * <p>
* By default, files are created by <code>File.createTempFile()</code> in
* the directory specified.
* </p>
*/
public static class DefaultTempFile implements TempFile {
- private File file;
+ private final File file;
- private OutputStream fstream;
+ private final OutputStream fstream;
public DefaultTempFile(String tempdir) throws IOException {
- file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
- fstream = new FileOutputStream(file);
+ this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
+ this.fstream = new FileOutputStream(this.file);
}
@Override
- public OutputStream open() throws Exception {
- return fstream;
+ public void delete() throws Exception {
+ safeClose(this.fstream);
+ this.file.delete();
}
@Override
- public void delete() throws Exception {
- safeClose(fstream);
- file.delete();
+ public String getName() {
+ return this.file.getAbsolutePath();
}
@Override
- public String getName() {
- return file.getAbsolutePath();
+ public OutputStream open() throws Exception {
+ return this.fstream;
}
}
/**
- * HTTP response. Return one of these from serve().
+ * Default strategy for creating and cleaning up temporary files.
+ * <p/>
+ * <p>
+ * This class stores its files in the standard location (that is, wherever
+ * <code>java.io.tmpdir</code> points to). Files are added to an internal
+ * list, and deleted when no longer needed (that is, when
+ * <code>clear()</code> is invoked at the end of processing a request).
+ * </p>
*/
- public static class Response {
-
- /**
- * HTTP status code after processing, e.g. "200 OK", Status.OK
- */
- private IStatus status;
-
- /**
- * MIME type of content, e.g. "text/html"
- */
- private String mimeType;
-
- /**
- * Data of the response, may be null.
- */
- private InputStream data;
-
- /**
- * Headers for the HTTP response. Use addHeader() to add lines.
- */
- private Map<String, String> header = new HashMap<String, String>();
-
- /**
- * The request method that spawned this response.
- */
- private Method requestMethod;
-
- /**
- * Use chunkedTransfer
- */
- private boolean chunkedTransfer;
-
- /**
- * Default constructor: response = Status.OK, mime = MIME_HTML and your
- * supplied message
- */
- public Response(String msg) {
- this(Status.OK, MIME_HTML, msg);
- }
-
- /**
- * Basic constructor.
- */
- public Response(IStatus status, String mimeType, InputStream data) {
- this.status = status;
- this.mimeType = mimeType;
- this.data = data;
- }
-
- /**
- * Convenience method that makes an InputStream out of given text.
- */
- public Response(IStatus status, String mimeType, String txt) {
- this.status = status;
- this.mimeType = mimeType;
- try {
- this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
- } catch (java.io.UnsupportedEncodingException uee) {
- LOG.log(Level.SEVERE, "encoding problem", uee);
- }
- }
-
- /**
- * Adds given line to the header.
- */
- public void addHeader(String name, String value) {
- header.put(name, value);
- }
-
- public String getHeader(String name) {
- return header.get(name);
- }
-
- /**
- * Sends given response to the socket.
- */
- protected void send(OutputStream outputStream) {
- String mime = mimeType;
- SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
- gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
-
- try {
- if (status == null) {
- throw new Error("sendResponse(): Status can't be null.");
- }
- PrintWriter pw = new PrintWriter(outputStream);
- pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
-
- if (mime != null) {
- pw.print("Content-Type: " + mime + "\r\n");
- }
-
- if (header == null || header.get("Date") == null) {
- pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
- }
-
- if (header != null) {
- for (String key : header.keySet()) {
- String value = header.get(key);
- pw.print(key + ": " + value + "\r\n");
- }
- }
-
- sendConnectionHeaderIfNotAlreadyPresent(pw, header);
-
- if (requestMethod != Method.HEAD && chunkedTransfer) {
- sendAsChunked(outputStream, pw);
- } else {
- int pending = data != null ? data.available() : 0;
- pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
- pw.print("\r\n");
- pw.flush();
- sendAsFixedLength(outputStream, pending);
- }
- outputStream.flush();
- safeClose(data);
- } catch (IOException ioe) {
- LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
- }
- }
-
- protected int sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) {
- for (String headerName : header.keySet()) {
- if (headerName.equalsIgnoreCase("content-length")) {
- try {
- return Integer.parseInt(header.get(headerName));
- } catch (NumberFormatException ex) {
- return size;
- }
- }
- }
-
- pw.print("Content-Length: " + size + "\r\n");
- return size;
- }
+ public static class DefaultTempFileManager implements TempFileManager {
- protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
- if (!headerAlreadySent(header, "connection")) {
- pw.print("Connection: keep-alive\r\n");
- }
- }
+ private final String tmpdir;
- private boolean headerAlreadySent(Map<String, String> header, String name) {
- boolean alreadySent = false;
- for (String headerName : header.keySet()) {
- alreadySent |= headerName.equalsIgnoreCase(name);
- }
- return alreadySent;
- }
+ private final List<TempFile> tempFiles;
- 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());
+ public DefaultTempFileManager() {
+ this.tmpdir = System.getProperty("java.io.tmpdir");
+ this.tempFiles = new ArrayList<TempFile>();
}
- private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
- 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;
+ @Override
+ public void clear() {
+ for (TempFile file : this.tempFiles) {
+ try {
+ file.delete();
+ } catch (Exception ignored) {
+ NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
}
}
+ this.tempFiles.clear();
}
- public IStatus getStatus() {
- return status;
- }
-
- public void setStatus(IStatus status) {
- this.status = status;
- }
-
- public String getMimeType() {
- return mimeType;
- }
-
- public void setMimeType(String mimeType) {
- this.mimeType = mimeType;
- }
-
- public InputStream getData() {
- return data;
- }
-
- public void setData(InputStream data) {
- this.data = data;
- }
-
- public Method getRequestMethod() {
- return requestMethod;
- }
-
- public void setRequestMethod(Method requestMethod) {
- this.requestMethod = requestMethod;
- }
-
- public void setChunkedTransfer(boolean chunkedTransfer) {
- this.chunkedTransfer = chunkedTransfer;
- }
-
- public interface IStatus {
-
- int getRequestStatus();
-
- String getDescription();
- }
-
- /**
- * Some HTTP response status codes
- */
- public enum Status implements IStatus {
- SWITCH_PROTOCOL(101, "Switching Protocols"),
- OK(200, "OK"),
- CREATED(201, "Created"),
- ACCEPTED(202, "Accepted"),
- NO_CONTENT(204, "No Content"),
- PARTIAL_CONTENT(206, "Partial Content"),
- REDIRECT(301, "Moved Permanently"),
- NOT_MODIFIED(304, "Not Modified"),
- BAD_REQUEST(400, "Bad Request"),
- UNAUTHORIZED(401, "Unauthorized"),
- FORBIDDEN(403, "Forbidden"),
- NOT_FOUND(404, "Not Found"),
- METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
- RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
- INTERNAL_ERROR(500, "Internal Server Error");
-
- private final int requestStatus;
-
- private final String description;
-
- Status(int requestStatus, String description) {
- this.requestStatus = requestStatus;
- this.description = description;
- }
-
- @Override
- public int getRequestStatus() {
- return this.requestStatus;
- }
-
- @Override
- public String getDescription() {
- return "" + this.requestStatus + " " + description;
- }
- }
- }
-
- public static final class ResponseException extends Exception {
-
- private static final long serialVersionUID = 6569838532917408380L;
-
- 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;
+ @Override
+ public TempFile createTempFile() throws Exception {
+ DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
+ this.tempFiles.add(tempFile);
+ return tempFile;
}
}
@@ -1021,40 +372,6 @@ public abstract class NanoHTTPD {
}
}
- /**
- * Handles one session, i.e. parses the HTTP request and returns the
- * response.
- */
- public interface IHTTPSession {
-
- void execute() throws IOException;
-
- Map<String, String> getParms();
-
- Map<String, String> getHeaders();
-
- /**
- * @return the path part of the URL.
- */
- String getUri();
-
- String getQueryParameterString();
-
- Method getMethod();
-
- InputStream getInputStream();
-
- CookieHandler getCookies();
-
- /**
- * Adds the files in the request body to the files map.
- *
- * @param files
- * map to modify
- */
- void parseBody(Map<String, String> files) throws IOException, ResponseException;
- }
-
protected class HTTPSession implements IHTTPSession {
public static final int BUFSIZE = 8192;
@@ -1063,7 +380,7 @@ public abstract class NanoHTTPD {
private final OutputStream outputStream;
- private PushbackInputStream inputStream;
+ private final PushbackInputStream inputStream;
private int splitbyte;
@@ -1085,207 +402,16 @@ public abstract class NanoHTTPD {
public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
this.tempFileManager = tempFileManager;
- this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+ this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
}
public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
this.tempFileManager = tempFileManager;
- this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+ this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
- remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
- headers = new HashMap<String, String>();
- }
-
- @Override
- public void execute() throws IOException {
- try {
- // Read the first 8192 bytes.
- // The full header should fit in here.
- // Apache's default header limit is 8KB.
- // Do NOT assume that a single read will get the entire header
- // at once!
- byte[] buf = new byte[BUFSIZE];
- splitbyte = 0;
- rlen = 0;
- {
- int read = -1;
- try {
- read = inputStream.read(buf, 0, BUFSIZE);
- } catch (Exception e) {
- safeClose(inputStream);
- safeClose(outputStream);
- throw new SocketException("NanoHttpd Shutdown");
- }
- if (read == -1) {
- // socket was been closed
- safeClose(inputStream);
- safeClose(outputStream);
- throw new SocketException("NanoHttpd Shutdown");
- }
- while (read > 0) {
- rlen += read;
- splitbyte = findHeaderEnd(buf, rlen);
- if (splitbyte > 0)
- break;
- read = inputStream.read(buf, rlen, BUFSIZE - rlen);
- }
- }
-
- if (splitbyte < rlen) {
- inputStream.unread(buf, splitbyte, rlen - splitbyte);
- }
-
- parms = new HashMap<String, String>();
- if (null == headers) {
- headers = new HashMap<String, String>();
- } else {
- headers.clear();
- }
-
- if (null != remoteIp) {
- headers.put("remote-addr", remoteIp);
- headers.put("http-client-ip", remoteIp);
- }
-
- // Create a BufferedReader for parsing the header.
- BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
-
- // Decode the header into parms and header java properties
- Map<String, String> pre = new HashMap<String, String>();
- decodeHeader(hin, pre, parms, headers);
-
- method = Method.lookup(pre.get("method"));
- if (method == null) {
- throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
- }
-
- uri = pre.get("uri");
-
- cookies = new CookieHandler(headers);
-
- // Ok, now do the serve()
- Response r = serve(this);
- if (r == null) {
- throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
- } else {
- cookies.unloadQueue(r);
- r.setRequestMethod(method);
- r.send(outputStream);
- }
- } catch (SocketException e) {
- // throw it out to close socket object (finalAccept)
- throw e;
- } catch (SocketTimeoutException ste) {
- // treat socket timeouts the same way we treat socket exceptions
- // i.e. close the stream & finalAccept object by throwing the
- // exception up the call stack.
- throw ste;
- } catch (IOException ioe) {
- Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
- r.send(outputStream);
- safeClose(outputStream);
- } catch (ResponseException re) {
- Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
- r.send(outputStream);
- safeClose(outputStream);
- } finally {
- tempFileManager.clear();
- }
- }
-
- @Override
- public void parseBody(Map<String, String> files) throws IOException, ResponseException {
- RandomAccessFile randomAccessFile = null;
- BufferedReader in = null;
- try {
-
- randomAccessFile = getTmpBucket();
-
- long size;
- if (headers.containsKey("content-length")) {
- size = Integer.parseInt(headers.get("content-length"));
- } else if (splitbyte < rlen) {
- size = rlen - splitbyte;
- } else {
- size = 0;
- }
-
- // Now read all the body and write it to f
- byte[] buf = new byte[512];
- while (rlen >= 0 && size > 0) {
- rlen = inputStream.read(buf, 0, (int) Math.min(size, 512));
- size -= rlen;
- if (rlen > 0) {
- randomAccessFile.write(buf, 0, rlen);
- }
- }
-
- // Get the raw body as a byte []
- ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
- randomAccessFile.seek(0);
-
- // Create a BufferedReader for easily reading it as string.
- InputStream bin = new FileInputStream(randomAccessFile.getFD());
- in = new BufferedReader(new InputStreamReader(bin));
-
- // If the method is POST, there may be parameters
- // in data section, too, read it:
- if (Method.POST.equals(method)) {
- String contentType = "";
- String contentTypeHeader = headers.get("content-type");
-
- StringTokenizer st = null;
- if (contentTypeHeader != null) {
- st = new StringTokenizer(contentTypeHeader, ",; ");
- if (st.hasMoreTokens()) {
- contentType = st.nextToken();
- }
- }
-
- if ("multipart/form-data".equalsIgnoreCase(contentType)) {
- // Handle multipart/form-data
- if (!st.hasMoreTokens()) {
- throw new ResponseException(Response.Status.BAD_REQUEST,
- "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
- }
-
- String boundaryStartString = "boundary=";
- int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
- String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
- if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
- boundary = boundary.substring(1, boundary.length() - 1);
- }
-
- decodeMultipartData(boundary, fbuf, in, parms, files);
- } else {
- String postLine = "";
- StringBuilder postLineBuffer = new StringBuilder();
- char pbuf[] = new char[512];
- int read = in.read(pbuf);
- while (read >= 0 && !postLine.endsWith("\r\n")) {
- postLine = String.valueOf(pbuf, 0, read);
- postLineBuffer.append(postLine);
- read = in.read(pbuf);
- }
- postLine = postLineBuffer.toString().trim();
- // Handle application/x-www-form-urlencoded
- if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
- decodeParms(postLine, parms);
- } else if (postLine.length() != 0) {
- // Special case for raw POST data => create a
- // special files entry "postData" with raw content
- // data
- files.put("postData", postLine);
- }
- }
- } else if (Method.PUT.equals(method)) {
- files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
- }
- } finally {
- safeClose(randomAccessFile);
- safeClose(in);
- }
+ this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
+ this.headers = new HashMap<String, String>();
}
/**
@@ -1326,13 +452,14 @@ public abstract class NanoHTTPD {
// NOTE: this now forces header names lower case since they are
// case insensitive and vary by client.
if (!st.hasMoreTokens()) {
- LOG.log(Level.FINE, "no protocol version specified, strange..");
+ NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange..");
}
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
- if (p >= 0)
+ if (p >= 0) {
headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
+ }
line = in.readLine();
}
@@ -1426,6 +553,129 @@ public abstract class NanoHTTPD {
}
/**
+ * Decodes parameters in percent-encoded URI-format ( e.g.
+ * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
+ * Map. NOTE: this doesn't support multiple identical keys due to the
+ * simplicity of Map.
+ */
+ private void decodeParms(String parms, Map<String, String> p) {
+ if (parms == null) {
+ this.queryParameterString = "";
+ return;
+ }
+
+ this.queryParameterString = parms;
+ StringTokenizer st = new StringTokenizer(parms, "&");
+ while (st.hasMoreTokens()) {
+ String e = st.nextToken();
+ int sep = e.indexOf('=');
+ if (sep >= 0) {
+ p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
+ } else {
+ p.put(decodePercent(e).trim(), "");
+ }
+ }
+ }
+
+ @Override
+ public void execute() throws IOException {
+ try {
+ // Read the first 8192 bytes.
+ // The full header should fit in here.
+ // Apache's default header limit is 8KB.
+ // Do NOT assume that a single read will get the entire header
+ // at once!
+ byte[] buf = new byte[HTTPSession.BUFSIZE];
+ this.splitbyte = 0;
+ this.rlen = 0;
+ {
+ int read = -1;
+ try {
+ read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
+ } catch (Exception e) {
+ safeClose(this.inputStream);
+ safeClose(this.outputStream);
+ throw new SocketException("NanoHttpd Shutdown");
+ }
+ if (read == -1) {
+ // socket was been closed
+ safeClose(this.inputStream);
+ safeClose(this.outputStream);
+ throw new SocketException("NanoHttpd Shutdown");
+ }
+ while (read > 0) {
+ this.rlen += read;
+ this.splitbyte = findHeaderEnd(buf, this.rlen);
+ if (this.splitbyte > 0) {
+ break;
+ }
+ read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
+ }
+ }
+
+ if (this.splitbyte < this.rlen) {
+ this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte);
+ }
+
+ this.parms = new HashMap<String, String>();
+ if (null == this.headers) {
+ this.headers = new HashMap<String, String>();
+ } else {
+ this.headers.clear();
+ }
+
+ if (null != this.remoteIp) {
+ this.headers.put("remote-addr", this.remoteIp);
+ this.headers.put("http-client-ip", this.remoteIp);
+ }
+
+ // Create a BufferedReader for parsing the header.
+ BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
+
+ // Decode the header into parms and header java properties
+ Map<String, String> pre = new HashMap<String, String>();
+ decodeHeader(hin, pre, this.parms, this.headers);
+
+ this.method = Method.lookup(pre.get("method"));
+ if (this.method == null) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
+ }
+
+ this.uri = pre.get("uri");
+
+ this.cookies = new CookieHandler(this.headers);
+
+ // Ok, now do the serve()
+ Response r = serve(this);
+ if (r == null) {
+ throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
+ } else {
+ this.cookies.unloadQueue(r);
+ r.setRequestMethod(this.method);
+ r.send(this.outputStream);
+ }
+ } catch (SocketException e) {
+ // throw it out to close socket object (finalAccept)
+ throw e;
+ } catch (SocketTimeoutException ste) {
+ // treat socket timeouts the same way we treat socket exceptions
+ // i.e. close the stream & finalAccept object by throwing the
+ // exception up the call stack.
+ throw ste;
+ } catch (IOException ioe) {
+ Response r = new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ r.send(this.outputStream);
+ safeClose(this.outputStream);
+ } catch (ResponseException re) {
+ Response r = new Response(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
+ r.send(this.outputStream);
+ safeClose(this.outputStream);
+ } finally {
+ this.tempFileManager.clear();
+ }
+ }
+
+ /**
* Find byte index separating header from body. It must be the last byte
* of the first two sequential new lines.
*/
@@ -1449,8 +699,9 @@ public abstract class NanoHTTPD {
List<Integer> matchbytes = new ArrayList<Integer>();
for (int i = 0; i < b.limit(); i++) {
if (b.get(i) == boundary[matchcount]) {
- if (matchcount == 0)
+ if (matchcount == 0) {
matchbyte = i;
+ }
matchcount++;
if (matchcount == boundary.length) {
matchbytes.add(matchbyte);
@@ -1470,6 +721,144 @@ public abstract class NanoHTTPD {
return ret;
}
+ @Override
+ public CookieHandler getCookies() {
+ return this.cookies;
+ }
+
+ @Override
+ public final Map<String, String> getHeaders() {
+ return this.headers;
+ }
+
+ @Override
+ public final InputStream getInputStream() {
+ return this.inputStream;
+ }
+
+ @Override
+ public final Method getMethod() {
+ return this.method;
+ }
+
+ @Override
+ public final Map<String, String> getParms() {
+ return this.parms;
+ }
+
+ @Override
+ public String getQueryParameterString() {
+ return this.queryParameterString;
+ }
+
+ private RandomAccessFile getTmpBucket() {
+ try {
+ TempFile tempFile = this.tempFileManager.createTempFile();
+ return new RandomAccessFile(tempFile.getName(), "rw");
+ } catch (Exception e) {
+ throw new Error(e); // we won't recover, so throw an error
+ }
+ }
+
+ @Override
+ public final String getUri() {
+ return this.uri;
+ }
+
+ @Override
+ public void parseBody(Map<String, String> files) throws IOException, ResponseException {
+ RandomAccessFile randomAccessFile = null;
+ BufferedReader in = null;
+ try {
+
+ randomAccessFile = getTmpBucket();
+
+ long size;
+ if (this.headers.containsKey("content-length")) {
+ size = Integer.parseInt(this.headers.get("content-length"));
+ } else if (this.splitbyte < this.rlen) {
+ size = this.rlen - this.splitbyte;
+ } else {
+ size = 0;
+ }
+
+ // Now read all the body and write it to f
+ byte[] buf = new byte[512];
+ while (this.rlen >= 0 && size > 0) {
+ this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, 512));
+ size -= this.rlen;
+ if (this.rlen > 0) {
+ randomAccessFile.write(buf, 0, this.rlen);
+ }
+ }
+
+ // Get the raw body as a byte []
+ ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
+ randomAccessFile.seek(0);
+
+ // Create a BufferedReader for easily reading it as string.
+ InputStream bin = new FileInputStream(randomAccessFile.getFD());
+ in = new BufferedReader(new InputStreamReader(bin));
+
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if (Method.POST.equals(this.method)) {
+ String contentType = "";
+ String contentTypeHeader = this.headers.get("content-type");
+
+ StringTokenizer st = null;
+ if (contentTypeHeader != null) {
+ st = new StringTokenizer(contentTypeHeader, ",; ");
+ if (st.hasMoreTokens()) {
+ contentType = st.nextToken();
+ }
+ }
+
+ if ("multipart/form-data".equalsIgnoreCase(contentType)) {
+ // Handle multipart/form-data
+ if (!st.hasMoreTokens()) {
+ throw new ResponseException(Response.Status.BAD_REQUEST,
+ "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
+ }
+
+ String boundaryStartString = "boundary=";
+ int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
+ String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
+ if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
+ boundary = boundary.substring(1, boundary.length() - 1);
+ }
+
+ decodeMultipartData(boundary, fbuf, in, this.parms, files);
+ } else {
+ String postLine = "";
+ StringBuilder postLineBuffer = new StringBuilder();
+ char pbuf[] = new char[512];
+ int read = in.read(pbuf);
+ while (read >= 0 && !postLine.endsWith("\r\n")) {
+ postLine = String.valueOf(pbuf, 0, read);
+ postLineBuffer.append(postLine);
+ read = in.read(pbuf);
+ }
+ postLine = postLineBuffer.toString().trim();
+ // Handle application/x-www-form-urlencoded
+ if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
+ decodeParms(postLine, this.parms);
+ } else if (postLine.length() != 0) {
+ // Special case for raw POST data => create a
+ // special files entry "postData" with raw content
+ // data
+ files.put("postData", postLine);
+ }
+ }
+ } else if (Method.PUT.equals(this.method)) {
+ files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
+ }
+ } finally {
+ safeClose(randomAccessFile);
+ safeClose(in);
+ }
+ }
+
/**
* Retrieves the content of a sent file and saves it to a temporary
* file. The full path to the saved file is returned.
@@ -1479,7 +868,7 @@ public abstract class NanoHTTPD {
if (len > 0) {
FileOutputStream fileOutputStream = null;
try {
- TempFile tempFile = tempFileManager.createTempFile();
+ TempFile tempFile = this.tempFileManager.createTempFile();
ByteBuffer src = b.duplicate();
fileOutputStream = new FileOutputStream(tempFile.getName());
FileChannel dest = fileOutputStream.getChannel();
@@ -1495,15 +884,6 @@ public abstract class NanoHTTPD {
return path;
}
- private RandomAccessFile getTmpBucket() {
- try {
- TempFile tempFile = tempFileManager.createTempFile();
- return new RandomAccessFile(tempFile.getName(), "rw");
- } catch (Exception e) {
- throw new Error(e); // we won't recover, so throw an error
- }
- }
-
/**
* It returns the offset separating multipart file headers from the
* file's data.
@@ -1517,184 +897,813 @@ public abstract class NanoHTTPD {
}
return i + 1;
}
+ }
+
+ /**
+ * Handles one session, i.e. parses the HTTP request and returns the
+ * response.
+ */
+ public interface IHTTPSession {
+
+ void execute() throws IOException;
+
+ CookieHandler getCookies();
+
+ Map<String, String> getHeaders();
+
+ InputStream getInputStream();
+
+ Method getMethod();
+
+ Map<String, String> getParms();
+
+ String getQueryParameterString();
/**
- * Decodes parameters in percent-encoded URI-format ( e.g.
- * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
- * Map. NOTE: this doesn't support multiple identical keys due to the
- * simplicity of Map.
+ * @return the path part of the URL.
*/
- private void decodeParms(String parms, Map<String, String> p) {
- if (parms == null) {
- queryParameterString = "";
- return;
+ String getUri();
+
+ /**
+ * Adds the files in the request body to the files map.
+ *
+ * @param files
+ * map to modify
+ */
+ void parseBody(Map<String, String> files) throws IOException, ResponseException;
+ }
+
+ /**
+ * HTTP Request methods, with the ability to decode a <code>String</code>
+ * back to its enum value.
+ */
+ public enum Method {
+ GET,
+ PUT,
+ POST,
+ DELETE,
+ HEAD,
+ OPTIONS;
+
+ static Method lookup(String method) {
+ for (Method m : Method.values()) {
+ if (m.toString().equalsIgnoreCase(method)) {
+ return m;
+ }
}
+ return null;
+ }
+ }
- queryParameterString = parms;
- StringTokenizer st = new StringTokenizer(parms, "&");
- while (st.hasMoreTokens()) {
- String e = st.nextToken();
- int sep = e.indexOf('=');
- if (sep >= 0) {
- p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
+ /**
+ * HTTP response. Return one of these from serve().
+ */
+ public static class Response {
+
+ public interface IStatus {
+
+ String getDescription();
+
+ int getRequestStatus();
+ }
+
+ /**
+ * Some HTTP response status codes
+ */
+ public enum Status implements IStatus {
+ SWITCH_PROTOCOL(101, "Switching Protocols"),
+ OK(200, "OK"),
+ CREATED(201, "Created"),
+ ACCEPTED(202, "Accepted"),
+ NO_CONTENT(204, "No Content"),
+ PARTIAL_CONTENT(206, "Partial Content"),
+ REDIRECT(301, "Moved Permanently"),
+ NOT_MODIFIED(304, "Not Modified"),
+ BAD_REQUEST(400, "Bad Request"),
+ UNAUTHORIZED(401, "Unauthorized"),
+ FORBIDDEN(403, "Forbidden"),
+ NOT_FOUND(404, "Not Found"),
+ METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
+ RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
+ INTERNAL_ERROR(500, "Internal Server Error");
+
+ private final int requestStatus;
+
+ private final String description;
+
+ Status(int requestStatus, String description) {
+ this.requestStatus = requestStatus;
+ this.description = description;
+ }
+
+ @Override
+ public String getDescription() {
+ return "" + this.requestStatus + " " + this.description;
+ }
+
+ @Override
+ public int getRequestStatus() {
+ return this.requestStatus;
+ }
+ }
+
+ /**
+ * HTTP status code after processing, e.g. "200 OK", Status.OK
+ */
+ private IStatus status;
+
+ /**
+ * MIME type of content, e.g. "text/html"
+ */
+ private String mimeType;
+
+ /**
+ * Data of the response, may be null.
+ */
+ private InputStream data;
+
+ /**
+ * Headers for the HTTP response. Use addHeader() to add lines.
+ */
+ private final Map<String, String> header = new HashMap<String, String>();
+
+ /**
+ * The request method that spawned this response.
+ */
+ private Method requestMethod;
+
+ /**
+ * Use chunkedTransfer
+ */
+ private boolean chunkedTransfer;
+
+ /**
+ * Basic constructor.
+ */
+ public Response(IStatus status, String mimeType, InputStream data) {
+ this.status = status;
+ this.mimeType = mimeType;
+ this.data = data;
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of given text.
+ */
+ public Response(IStatus status, String mimeType, String txt) {
+ this.status = status;
+ this.mimeType = mimeType;
+ try {
+ this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
+ } catch (java.io.UnsupportedEncodingException uee) {
+ NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem", uee);
+ }
+ }
+
+ /**
+ * Default constructor: response = Status.OK, mime = MIME_HTML and your
+ * supplied message
+ */
+ public Response(String msg) {
+ this(Status.OK, NanoHTTPD.MIME_HTML, msg);
+ }
+
+ /**
+ * Adds given line to the header.
+ */
+ public void addHeader(String name, String value) {
+ this.header.put(name, value);
+ }
+
+ public InputStream getData() {
+ return this.data;
+ }
+
+ public String getHeader(String name) {
+ return this.header.get(name);
+ }
+
+ public String getMimeType() {
+ return this.mimeType;
+ }
+
+ public Method getRequestMethod() {
+ return this.requestMethod;
+ }
+
+ public IStatus getStatus() {
+ return this.status;
+ }
+
+ private boolean headerAlreadySent(Map<String, String> header, String name) {
+ boolean alreadySent = false;
+ for (String headerName : header.keySet()) {
+ alreadySent |= headerName.equalsIgnoreCase(name);
+ }
+ return alreadySent;
+ }
+
+ /**
+ * Sends given response to the socket.
+ */
+ protected void send(OutputStream outputStream) {
+ String mime = this.mimeType;
+ SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+ gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ try {
+ if (this.status == null) {
+ throw new Error("sendResponse(): Status can't be null.");
+ }
+ PrintWriter pw = new PrintWriter(outputStream);
+ pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n");
+
+ if (mime != null) {
+ pw.print("Content-Type: " + mime + "\r\n");
+ }
+
+ if (this.header == null || this.header.get("Date") == null) {
+ pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
+ }
+
+ if (this.header != null) {
+ for (String key : this.header.keySet()) {
+ String value = this.header.get(key);
+ pw.print(key + ": " + value + "\r\n");
+ }
+ }
+
+ sendConnectionHeaderIfNotAlreadyPresent(pw, this.header);
+
+ if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
+ sendAsChunked(outputStream, pw);
} else {
- p.put(decodePercent(e).trim(), "");
+ int pending = this.data != null ? this.data.available() : 0;
+ pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
+ pw.print("\r\n");
+ pw.flush();
+ sendAsFixedLength(outputStream, pending);
}
+ outputStream.flush();
+ safeClose(this.data);
+ } catch (IOException ioe) {
+ NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
}
}
- @Override
- public final Map<String, String> getParms() {
- return parms;
+ 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 = this.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());
}
- public String getQueryParameterString() {
- return queryParameterString;
+ private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
+ if (this.requestMethod != Method.HEAD && this.data != null) {
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] buff = new byte[BUFFER_SIZE];
+ while (pending > 0) {
+ int read = this.data.read(buff, 0, pending > BUFFER_SIZE ? BUFFER_SIZE : pending);
+ if (read <= 0) {
+ break;
+ }
+ outputStream.write(buff, 0, read);
+ pending -= read;
+ }
+ }
}
- @Override
- public final Map<String, String> getHeaders() {
- return headers;
+ protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
+ if (!headerAlreadySent(header, "connection")) {
+ pw.print("Connection: keep-alive\r\n");
+ }
}
- @Override
- public final String getUri() {
- return uri;
+ protected int sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) {
+ for (String headerName : header.keySet()) {
+ if (headerName.equalsIgnoreCase("content-length")) {
+ try {
+ return Integer.parseInt(header.get(headerName));
+ } catch (NumberFormatException ex) {
+ return size;
+ }
+ }
+ }
+
+ pw.print("Content-Length: " + size + "\r\n");
+ return size;
}
- @Override
- public final Method getMethod() {
- return method;
+ public void setChunkedTransfer(boolean chunkedTransfer) {
+ this.chunkedTransfer = chunkedTransfer;
}
- @Override
- public final InputStream getInputStream() {
- return inputStream;
+ public void setData(InputStream data) {
+ this.data = data;
}
- @Override
- public CookieHandler getCookies() {
- return cookies;
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ public void setRequestMethod(Method requestMethod) {
+ this.requestMethod = requestMethod;
+ }
+
+ public void setStatus(IStatus status) {
+ this.status = status;
}
}
- public static class Cookie {
+ public static final class ResponseException extends Exception {
+
+ private static final long serialVersionUID = 6569838532917408380L;
- private String n, v, e;
+ private final Response.Status status;
- public Cookie(String name, String value, String expires) {
- n = name;
- v = value;
- e = expires;
+ public ResponseException(Response.Status status, String message) {
+ super(message);
+ this.status = status;
}
- public Cookie(String name, String value) {
- this(name, value, 30);
+ public ResponseException(Response.Status status, String message, Exception e) {
+ super(message, e);
+ this.status = status;
}
- public Cookie(String name, String value, int numDays) {
- n = name;
- v = value;
- e = getHTTPTime(numDays);
+ public Response.Status getStatus() {
+ return this.status;
}
+ }
- public String getHTTPHeader() {
- String fmt = "%s=%s; expires=%s";
- return String.format(fmt, n, v, e);
+ /**
+ * A temp file.
+ * <p/>
+ * <p>
+ * Temp files are responsible for managing the actual temporary storage and
+ * cleaning themselves up when no longer needed.
+ * </p>
+ */
+ public interface TempFile {
+
+ void delete() throws Exception;
+
+ String getName();
+
+ OutputStream open() throws Exception;
+ }
+
+ /**
+ * Temp file manager.
+ * <p/>
+ * <p>
+ * 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.
+ * </p>
+ */
+ public interface TempFileManager {
+
+ void clear();
+
+ TempFile createTempFile() throws Exception;
+ }
+
+ /**
+ * Factory to create temp file managers.
+ */
+ public interface TempFileManagerFactory {
+
+ TempFileManager create();
+ }
+
+ /**
+ * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
+ * This is required as the Keep-Alive HTTP connections would otherwise block
+ * the socket reading thread forever (or as long the browser is open).
+ */
+ public static final int SOCKET_READ_TIMEOUT = 5000;
+
+ /**
+ * Common MIME type for dynamic content: plain text
+ */
+ public static final String MIME_PLAINTEXT = "text/plain";
+
+ /**
+ * Common MIME type for dynamic content: html
+ */
+ public static final String MIME_HTML = "text/html";
+
+ /**
+ * Pseudo-Parameter to use to store the actual query string in the
+ * parameters map for later re-processing.
+ */
+ private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
+
+ /**
+ * logger to log to.
+ */
+ private static Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
+
+ /**
+ * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
+ * array of loaded KeyManagers. These objects must properly
+ * loaded/initialized by the caller.
+ */
+ public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
+ SSLServerSocketFactory res = null;
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(loadedKeyStore);
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
+ res = ctx.getServerSocketFactory();
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
}
+ return res;
+ }
- 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());
+ /**
+ * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
+ * loaded KeyManagerFactory. These objects must properly loaded/initialized
+ * by the caller.
+ */
+ public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
+ SSLServerSocketFactory res = null;
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(loadedKeyStore);
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
+ res = ctx.getServerSocketFactory();
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
}
+ return res;
}
/**
- * Provides rudimentary support for cookies. Doesn't support 'path',
- * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported
- * features.
- *
- * @author LordFokas
+ * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
+ * certificate and passphrase
*/
- public class CookieHandler implements Iterable<String> {
+ public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
+ SSLServerSocketFactory res = null;
+ try {
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
+ keystore.load(keystoreStream, passphrase);
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keystore, passphrase);
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
+ res = ctx.getServerSocketFactory();
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
+ }
+ return res;
+ }
- private HashMap<String, String> cookies = new HashMap<String, String>();
+ private static final void safeClose(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
+ }
+ }
+ }
- private ArrayList<Cookie> queue = new ArrayList<Cookie>();
+ private final String hostname;
- public CookieHandler(Map<String, String> httpHeaders) {
- String raw = httpHeaders.get("cookie");
- if (raw != null) {
- String[] tokens = raw.split(";");
- for (String token : tokens) {
- String[] data = token.trim().split("=");
- if (data.length == 2) {
- cookies.put(data[0], data[1]);
- }
+ private final int myPort;
+
+ private ServerSocket myServerSocket;
+
+ private final Set<Socket> openConnections = new HashSet<Socket>();
+
+ private SSLServerSocketFactory sslServerSocketFactory;
+
+ private Thread myThread;
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ private AsyncRunner asyncRunner;
+
+ /**
+ * Pluggable strategy for creating and cleaning up temporary files.
+ */
+ private TempFileManagerFactory tempFileManagerFactory;
+
+ /**
+ * Constructs an HTTP server on given port.
+ */
+ public NanoHTTPD(int port) {
+ this(null, port);
+ }
+
+ // -------------------------------------------------------------------------------
+ // //
+ //
+ // Threading Strategy.
+ //
+ // -------------------------------------------------------------------------------
+ // //
+
+ /**
+ * Constructs an HTTP server on given hostname and port.
+ */
+ public NanoHTTPD(String hostname, int port) {
+ this.hostname = hostname;
+ this.myPort = port;
+ setTempFileManagerFactory(new DefaultTempFileManagerFactory());
+ setAsyncRunner(new DefaultAsyncRunner());
+ }
+
+ // -------------------------------------------------------------------------------
+ // //
+ //
+ // Temp file handling strategy.
+ //
+ // -------------------------------------------------------------------------------
+ // //
+
+ /**
+ * Forcibly closes all connections that are open.
+ */
+ public synchronized void closeAllConnections() {
+ for (Socket socket : this.openConnections) {
+ safeClose(socket);
+ }
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter
+ * name might have been supplied several times, by return lists of values.
+ * In general these lists will contain a single element.
+ *
+ * @param parms
+ * original <b>NanoHTTPD</b> parameters values, as passed to the
+ * <code>serve()</code> method.
+ * @return a map of <code>String</code> (parameter name) to
+ * <code>List&lt;String&gt;</code> (a list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
+ return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER));
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter
+ * name might have been supplied several times, by return lists of values.
+ * In general these lists will contain a single element.
+ *
+ * @param queryString
+ * a query string pulled from the URL.
+ * @return a map of <code>String</code> (parameter name) to
+ * <code>List&lt;String&gt;</code> (a list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters(String queryString) {
+ Map<String, List<String>> parms = new HashMap<String, List<String>>();
+ if (queryString != null) {
+ StringTokenizer st = new StringTokenizer(queryString, "&");
+ while (st.hasMoreTokens()) {
+ String e = st.nextToken();
+ int sep = e.indexOf('=');
+ String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
+ if (!parms.containsKey(propertyName)) {
+ parms.put(propertyName, new ArrayList<String>());
+ }
+ String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
+ if (propertyValue != null) {
+ parms.get(propertyName).add(propertyValue);
}
}
}
+ return parms;
+ }
- @Override
- public Iterator<String> iterator() {
- return cookies.keySet().iterator();
+ /**
+ * Decode percent encoded <code>String</code> values.
+ *
+ * @param str
+ * the percent encoded <code>String</code>
+ * @return expanded form of the input, for example "foo%20bar" becomes
+ * "foo bar"
+ */
+ protected String decodePercent(String str) {
+ String decoded = null;
+ try {
+ decoded = URLDecoder.decode(str, "UTF8");
+ } catch (UnsupportedEncodingException ignored) {
+ NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
}
+ return decoded;
+ }
- /**
- * 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 final int getListeningPort() {
+ return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
+ }
- public void set(Cookie cookie) {
- queue.add(cookie);
+ public final boolean isAlive() {
+ return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
+ }
+
+ /**
+ * Call before start() to serve over HTTPS instead of HTTP
+ */
+ public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) {
+ this.sslServerSocketFactory = sslServerSocketFactory;
+ }
+
+ /**
+ * Registers that a new connection has been set up.
+ *
+ * @param socket
+ * the {@link Socket} for the connection.
+ */
+ public synchronized void registerConnection(Socket socket) {
+ this.openConnections.add(socket);
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this returns a 404 "Not Found" plain text error response.)
+ *
+ * @param session
+ * The HTTP session
+ * @return HTTP response, see class Response for details
+ */
+ public Response serve(IHTTPSession session) {
+ Map<String, String> files = new HashMap<String, String>();
+ Method method = session.getMethod();
+ if (Method.PUT.equals(method) || Method.POST.equals(method)) {
+ try {
+ session.parseBody(files);
+ } catch (IOException ioe) {
+ return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ } catch (ResponseException re) {
+ return new Response(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
+ }
}
- /**
- * 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);
+ Map<String, String> parms = session.getParms();
+ parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
+ return serve(session.getUri(), method, session.getHeaders(), parms, files);
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this returns a 404 "Not Found" plain text error response.)
+ *
+ * @param uri
+ * Percent-decoded URI without parameters, for example
+ * "/index.cgi"
+ * @param method
+ * "GET", "POST" etc.
+ * @param parms
+ * Parsed, percent decoded parameters from URI and, in case of
+ * POST, data.
+ * @param headers
+ * Header entries, percent decoded
+ * @return HTTP response, see class Response for details
+ */
+ @Deprecated
+ public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
+ return new Response(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
+ }
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ *
+ * @param asyncRunner
+ * new strategy for handling threads.
+ */
+ public void setAsyncRunner(AsyncRunner asyncRunner) {
+ this.asyncRunner = asyncRunner;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Start the server.
+ *
+ * @throws IOException
+ * if the socket is in use.
+ */
+ public void start() throws IOException {
+ if (this.sslServerSocketFactory != null) {
+ SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
+ ss.setNeedClientAuth(false);
+ this.myServerSocket = ss;
+ } else {
+ this.myServerSocket = new ServerSocket();
}
+ this.myServerSocket.setReuseAddress(true);
+ this.myServerSocket.bind(this.hostname != null ? new InetSocketAddress(this.hostname, this.myPort) : new InetSocketAddress(this.myPort));
- /**
- * 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());
+ this.myThread = new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ do {
+ try {
+ final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
+ registerConnection(finalAccept);
+ finalAccept.setSoTimeout(NanoHTTPD.SOCKET_READ_TIMEOUT);
+ final InputStream inputStream = finalAccept.getInputStream();
+ NanoHTTPD.this.asyncRunner.exec(new Runnable() {
+
+ @Override
+ public void run() {
+ OutputStream outputStream = null;
+ try {
+ outputStream = finalAccept.getOutputStream();
+ TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
+ HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
+ while (!finalAccept.isClosed()) {
+ session.execute();
+ }
+ } catch (Exception e) {
+ // When the socket is closed by the client,
+ // we throw our own SocketException
+ // to break the "keep alive" loop above. If
+ // the exception was anything other
+ // than the expected SocketException OR a
+ // SocketTimeoutException, print the
+ // stacktrace
+ if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
+ NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
+ }
+ } finally {
+ safeClose(outputStream);
+ safeClose(inputStream);
+ safeClose(finalAccept);
+ unRegisterConnection(finalAccept);
+ }
+ }
+ });
+ } catch (IOException e) {
+ NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
+ }
+ } while (!NanoHTTPD.this.myServerSocket.isClosed());
+ }
+ });
+ this.myThread.setDaemon(true);
+ this.myThread.setName("NanoHttpd Main Listener");
+ this.myThread.start();
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop() {
+ try {
+ safeClose(this.myServerSocket);
+ closeAllConnections();
+ if (this.myThread != null) {
+ this.myThread.join();
}
+ } catch (Exception e) {
+ NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
}
}
+
+ /**
+ * Registers that a connection has been closed
+ *
+ * @param socket
+ * the {@link Socket} for the connection.
+ */
+ public synchronized void unRegisterConnection(Socket socket) {
+ this.openConnections.remove(socket);
+ }
+
+ public final boolean wasStarted() {
+ return this.myServerSocket != null && this.myThread != null;
+ }
}
diff --git a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
index 92640a9..823dd92 100644
--- a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
+++ b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
@@ -33,7 +33,6 @@ package fi.iki.elonen;
* #L%
*/
-import static fi.iki.elonen.NanoHTTPD.MIME_HTML;
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
import java.io.BufferedReader;
@@ -59,11 +58,7 @@ public class MarkdownWebServerPlugin implements WebServerPlugin {
private final PegDownProcessor processor;
public MarkdownWebServerPlugin() {
- processor = new PegDownProcessor();
- }
-
- @Override
- public void initialize(Map<String, String> commandLineOptions) {
+ this.processor = new PegDownProcessor();
}
@Override
@@ -73,9 +68,7 @@ public class MarkdownWebServerPlugin implements WebServerPlugin {
}
@Override
- public NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, NanoHTTPD.IHTTPSession session, File file, String mimeType) {
- String markdownSource = readSource(file);
- return markdownSource == null ? null : new NanoHTTPD.Response(OK, MIME_HTML, processor.markdownToHtml(markdownSource));
+ public void initialize(Map<String, String> commandLineOptions) {
}
private String readSource(File file) {
@@ -95,7 +88,7 @@ public class MarkdownWebServerPlugin implements WebServerPlugin {
reader.close();
return sb.toString();
} catch (Exception e) {
- LOG.log(Level.SEVERE, "could not read source", e);
+ MarkdownWebServerPlugin.LOG.log(Level.SEVERE, "could not read source", e);
return null;
} finally {
try {
@@ -106,8 +99,14 @@ public class MarkdownWebServerPlugin implements WebServerPlugin {
reader.close();
}
} catch (IOException ignored) {
- LOG.log(Level.FINEST, "close failed", ignored);
+ MarkdownWebServerPlugin.LOG.log(Level.FINEST, "close failed", ignored);
}
}
}
+
+ @Override
+ public NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, NanoHTTPD.IHTTPSession session, File file, String mimeType) {
+ String markdownSource = readSource(file);
+ return markdownSource == null ? null : new NanoHTTPD.Response(OK, NanoHTTPD.MIME_HTML, this.processor.markdownToHtml(markdownSource));
+ }
}
diff --git a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
index d7f1c4c..29cdc1e 100644
--- a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
+++ b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
@@ -39,16 +39,16 @@ package fi.iki.elonen;
public class MarkdownWebServerPluginInfo implements WebServerPluginInfo {
@Override
- public String[] getMimeTypes() {
+ public String[] getIndexFilesForMimeType(String mime) {
return new String[]{
- "text/markdown"
+ "index.md"
};
}
@Override
- public String[] getIndexFilesForMimeType(String mime) {
+ public String[] getMimeTypes() {
return new String[]{
- "index.md"
+ "text/markdown"
};
}
diff --git a/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java b/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
index f5dcd91..3d4ca76 100644
--- a/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
+++ b/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
@@ -52,11 +52,11 @@ public class InternalRewrite extends Response {
this.uri = uri;
}
- public String getUri() {
- return uri;
+ public Map<String, String> getHeaders() {
+ return this.headers;
}
- public Map<String, String> getHeaders() {
- return headers;
+ public String getUri() {
+ return this.uri;
}
}
diff --git a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java b/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
index 2a4c699..bfc831c 100644
--- a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
+++ b/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
@@ -44,14 +44,6 @@ public class ServerRunner {
*/
private static Logger LOG = Logger.getLogger(ServerRunner.class.getName());
- public static <T extends NanoHTTPD> void run(Class<T> serverClass) {
- try {
- executeInstance((NanoHTTPD) serverClass.newInstance());
- } catch (Exception e) {
- LOG.log(Level.SEVERE, "Cound nor create server", e);
- }
- }
-
public static void executeInstance(NanoHTTPD server) {
try {
server.start();
@@ -70,4 +62,12 @@ public class ServerRunner {
server.stop();
System.out.println("Server stopped.\n");
}
+
+ public static <T extends NanoHTTPD> void run(Class<T> serverClass) {
+ try {
+ executeInstance(serverClass.newInstance());
+ } catch (Exception e) {
+ ServerRunner.LOG.log(Level.SEVERE, "Cound nor create server", e);
+ }
+ }
}
diff --git a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
index 147bbf9..d485fb1 100644
--- a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
+++ b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
@@ -122,33 +122,6 @@ public class SimpleWebServer extends NanoHTTPD {
private static Map<String, WebServerPlugin> mimeTypeHandlers = new HashMap<String, WebServerPlugin>();
- private final boolean quiet;
-
- protected List<File> rootDirs;
-
- public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
- super(host, port);
- this.quiet = quiet;
- this.rootDirs = new ArrayList<File>();
- this.rootDirs.add(wwwroot);
-
- this.init();
- }
-
- public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
- super(host, port);
- this.quiet = quiet;
- this.rootDirs = new ArrayList<File>(wwwroots);
-
- this.init();
- }
-
- /**
- * Used to initialize and customize the server.
- */
- public void init() {
- }
-
/**
* Starts as a standalone file server and waits for Enter.
*/
@@ -172,7 +145,7 @@ public class SimpleWebServer extends NanoHTTPD {
} else if (args[i].equalsIgnoreCase("-d") || args[i].equalsIgnoreCase("--dir")) {
rootDirs.add(new File(args[i + 1]).getAbsoluteFile());
} else if (args[i].equalsIgnoreCase("--licence")) {
- System.out.println(LICENCE + "\n");
+ System.out.println(SimpleWebServer.LICENCE + "\n");
} else if (args[i].startsWith("-X:")) {
int dot = args[i].indexOf('=');
if (dot > 0) {
@@ -234,15 +207,64 @@ public class SimpleWebServer extends NanoHTTPD {
int dot = filename.lastIndexOf('.');
if (dot >= 0) {
String extension = filename.substring(dot + 1).toLowerCase();
- MIME_TYPES.put(extension, mimeType);
+ SimpleWebServer.MIME_TYPES.put(extension, mimeType);
}
}
- INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
+ SimpleWebServer.INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
}
- mimeTypeHandlers.put(mimeType, plugin);
+ SimpleWebServer.mimeTypeHandlers.put(mimeType, plugin);
plugin.initialize(commandLineOptions);
}
+ private final boolean quiet;
+
+ protected List<File> rootDirs;
+
+ public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
+ super(host, port);
+ this.quiet = quiet;
+ this.rootDirs = new ArrayList<File>();
+ this.rootDirs.add(wwwroot);
+
+ init();
+ }
+
+ public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
+ super(host, port);
+ this.quiet = quiet;
+ this.rootDirs = new ArrayList<File>(wwwroots);
+
+ init();
+ }
+
+ private boolean canServeUri(String uri, File homeDir) {
+ boolean canServeUri;
+ File f = new File(homeDir, uri);
+ canServeUri = f.exists();
+ if (!canServeUri) {
+ String mimeTypeForFile = getMimeTypeForFile(uri);
+ WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile);
+ if (plugin != null) {
+ canServeUri = plugin.canServeUri(uri, homeDir);
+ }
+ }
+ return canServeUri;
+ }
+
+ // Announce that the file server accepts partial content requests
+ private Response createResponse(Response.Status status, String mimeType, InputStream message) {
+ Response res = new Response(status, mimeType, message);
+ res.addHeader("Accept-Ranges", "bytes");
+ return res;
+ }
+
+ // Announce that the file server accepts partial content requests
+ private Response createResponse(Response.Status status, String mimeType, String message) {
+ Response res = new Response(status, mimeType, message);
+ res.addHeader("Accept-Ranges", "bytes");
+ return res;
+ }
+
/**
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
* instead of '+'.
@@ -252,11 +274,11 @@ public class SimpleWebServer extends NanoHTTPD {
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
- if (tok.equals("/"))
+ if (tok.equals("/")) {
newUri += "/";
- else if (tok.equals(" "))
+ } else if (tok.equals(" ")) {
newUri += "%20";
- else {
+ } else {
try {
newUri += URLEncoder.encode(tok, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
@@ -266,33 +288,111 @@ public class SimpleWebServer extends NanoHTTPD {
return newUri;
}
- public Response serve(IHTTPSession session) {
- Map<String, String> header = session.getHeaders();
- Map<String, String> parms = session.getParms();
- String uri = session.getUri();
+ private String findIndexFileInDirectory(File directory) {
+ for (String fileName : SimpleWebServer.INDEX_FILE_NAMES) {
+ File indexFile = new File(directory, fileName);
+ if (indexFile.exists()) {
+ return fileName;
+ }
+ }
+ return null;
+ }
- if (!quiet) {
- System.out.println(session.getMethod() + " '" + uri + "' ");
+ protected Response getForbiddenResponse(String s) {
+ return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
+ }
- Iterator<String> e = header.keySet().iterator();
- while (e.hasNext()) {
- String value = e.next();
- System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'");
- }
- e = parms.keySet().iterator();
- while (e.hasNext()) {
- String value = e.next();
- System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'");
+ protected Response getInternalErrorResponse(String s) {
+ return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
+ }
+
+ // Get MIME type from file name extension, if possible
+ private String getMimeTypeForFile(String uri) {
+ int dot = uri.lastIndexOf('.');
+ String mime = null;
+ if (dot >= 0) {
+ mime = SimpleWebServer.MIME_TYPES.get(uri.substring(dot + 1).toLowerCase());
+ }
+ return mime == null ? SimpleWebServer.MIME_DEFAULT_BINARY : mime;
+ }
+
+ protected Response getNotFoundResponse() {
+ return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
+ }
+
+ /**
+ * Used to initialize and customize the server.
+ */
+ public void init() {
+ }
+
+ protected String listDirectory(String uri, File f) {
+ String heading = "Directory " + uri;
+ StringBuilder msg =
+ new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
+ + "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
+
+ String up = null;
+ if (uri.length() > 1) {
+ String u = uri.substring(0, uri.length() - 1);
+ int slash = u.lastIndexOf('/');
+ if (slash >= 0 && slash < u.length()) {
+ up = uri.substring(0, slash + 1);
}
}
- for (File homeDir : rootDirs) {
- // Make sure we won't die of an exception later
- if (!homeDir.isDirectory()) {
- return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
+ List<String> files = Arrays.asList(f.list(new FilenameFilter() {
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return new File(dir, name).isFile();
+ }
+ }));
+ Collections.sort(files);
+ List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return new File(dir, name).isDirectory();
+ }
+ }));
+ Collections.sort(directories);
+ if (up != null || directories.size() + files.size() > 0) {
+ msg.append("<ul>");
+ if (up != null || directories.size() > 0) {
+ msg.append("<section class=\"directories\">");
+ if (up != null) {
+ msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>");
+ }
+ for (String directory : directories) {
+ String dir = directory + "/";
+ msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir)
+ .append("</span></a></b></li>");
+ }
+ msg.append("</section>");
}
+ if (files.size() > 0) {
+ msg.append("<section class=\"files\">");
+ for (String file : files) {
+ msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
+ File curFile = new File(f, file);
+ long len = curFile.length();
+ msg.append("&nbsp;<span class=\"filesize\">(");
+ if (len < 1024) {
+ msg.append(len).append(" bytes");
+ } else if (len < 1024 * 1024) {
+ msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
+ } else {
+ msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
+ }
+ msg.append(")</span></li>");
+ }
+ msg.append("</section>");
+ }
+ msg.append("</ul>");
}
- return respond(Collections.unmodifiableMap(header), session, uri);
+ msg.append("</body></html>");
+ return msg.toString();
}
private Response respond(Map<String, String> headers, IHTTPSession session, String uri) {
@@ -309,8 +409,8 @@ public class SimpleWebServer extends NanoHTTPD {
boolean canServeUri = false;
File homeDir = null;
- for (int i = 0; !canServeUri && i < rootDirs.size(); i++) {
- homeDir = rootDirs.get(i);
+ for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) {
+ homeDir = this.rootDirs.get(i);
canServeUri = canServeUri(uri, homeDir);
}
if (!canServeUri) {
@@ -344,7 +444,7 @@ public class SimpleWebServer extends NanoHTTPD {
}
String mimeTypeForFile = getMimeTypeForFile(uri);
- WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile);
+ WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile);
Response response = null;
if (plugin != null) {
response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile);
@@ -358,30 +458,34 @@ public class SimpleWebServer extends NanoHTTPD {
return response != null ? response : getNotFoundResponse();
}
- protected Response getNotFoundResponse() {
- return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
- }
+ @Override
+ public Response serve(IHTTPSession session) {
+ Map<String, String> header = session.getHeaders();
+ Map<String, String> parms = session.getParms();
+ String uri = session.getUri();
- protected Response getForbiddenResponse(String s) {
- return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
- }
+ if (!this.quiet) {
+ System.out.println(session.getMethod() + " '" + uri + "' ");
- protected Response getInternalErrorResponse(String s) {
- return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
- }
+ Iterator<String> e = header.keySet().iterator();
+ while (e.hasNext()) {
+ String value = e.next();
+ System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'");
+ }
+ e = parms.keySet().iterator();
+ while (e.hasNext()) {
+ String value = e.next();
+ System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'");
+ }
+ }
- private boolean canServeUri(String uri, File homeDir) {
- boolean canServeUri;
- File f = new File(homeDir, uri);
- canServeUri = f.exists();
- if (!canServeUri) {
- String mimeTypeForFile = getMimeTypeForFile(uri);
- WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile);
- if (plugin != null) {
- canServeUri = plugin.canServeUri(uri, homeDir);
+ for (File homeDir : this.rootDirs) {
+ // Make sure we won't die of an exception later
+ if (!homeDir.isDirectory()) {
+ return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
}
}
- return canServeUri;
+ return respond(Collections.unmodifiableMap(header), session, uri);
}
/**
@@ -445,9 +549,9 @@ public class SimpleWebServer extends NanoHTTPD {
res.addHeader("ETag", etag);
}
} else {
- if (etag.equals(header.get("if-none-match")))
+ if (etag.equals(header.get("if-none-match"))) {
res = createResponse(Response.Status.NOT_MODIFIED, mime, "");
- else {
+ } else {
res = createResponse(Response.Status.OK, mime, new FileInputStream(file));
res.addHeader("Content-Length", "" + fileLen);
res.addHeader("ETag", etag);
@@ -459,107 +563,4 @@ public class SimpleWebServer extends NanoHTTPD {
return res;
}
-
- // Get MIME type from file name extension, if possible
- private String getMimeTypeForFile(String uri) {
- int dot = uri.lastIndexOf('.');
- String mime = null;
- if (dot >= 0) {
- mime = MIME_TYPES.get(uri.substring(dot + 1).toLowerCase());
- }
- return mime == null ? MIME_DEFAULT_BINARY : mime;
- }
-
- // Announce that the file server accepts partial content requests
- private Response createResponse(Response.Status status, String mimeType, InputStream message) {
- Response res = new Response(status, mimeType, message);
- res.addHeader("Accept-Ranges", "bytes");
- return res;
- }
-
- // Announce that the file server accepts partial content requests
- private Response createResponse(Response.Status status, String mimeType, String message) {
- Response res = new Response(status, mimeType, message);
- res.addHeader("Accept-Ranges", "bytes");
- return res;
- }
-
- private String findIndexFileInDirectory(File directory) {
- for (String fileName : INDEX_FILE_NAMES) {
- File indexFile = new File(directory, fileName);
- if (indexFile.exists()) {
- return fileName;
- }
- }
- return null;
- }
-
- protected String listDirectory(String uri, File f) {
- String heading = "Directory " + uri;
- StringBuilder msg =
- new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
- + "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
-
- String up = null;
- if (uri.length() > 1) {
- String u = uri.substring(0, uri.length() - 1);
- int slash = u.lastIndexOf('/');
- if (slash >= 0 && slash < u.length()) {
- up = uri.substring(0, slash + 1);
- }
- }
-
- List<String> files = Arrays.asList(f.list(new FilenameFilter() {
-
- @Override
- public boolean accept(File dir, String name) {
- return new File(dir, name).isFile();
- }
- }));
- Collections.sort(files);
- List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
-
- @Override
- public boolean accept(File dir, String name) {
- return new File(dir, name).isDirectory();
- }
- }));
- Collections.sort(directories);
- if (up != null || directories.size() + files.size() > 0) {
- msg.append("<ul>");
- if (up != null || directories.size() > 0) {
- msg.append("<section class=\"directories\">");
- if (up != null) {
- msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>");
- }
- for (String directory : directories) {
- String dir = directory + "/";
- msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir)
- .append("</span></a></b></li>");
- }
- msg.append("</section>");
- }
- if (files.size() > 0) {
- msg.append("<section class=\"files\">");
- for (String file : files) {
- msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
- File curFile = new File(f, file);
- long len = curFile.length();
- msg.append("&nbsp;<span class=\"filesize\">(");
- if (len < 1024) {
- msg.append(len).append(" bytes");
- } else if (len < 1024 * 1024) {
- msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
- } else {
- msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
- }
- msg.append(")</span></li>");
- }
- msg.append("</section>");
- }
- msg.append("</ul>");
- }
- msg.append("</body></html>");
- return msg.toString();
- }
}
diff --git a/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java b/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
index cdfc3ff..8b490d3 100644
--- a/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
+++ b/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
@@ -43,9 +43,9 @@ import fi.iki.elonen.NanoHTTPD.IHTTPSession;
*/
public interface WebServerPlugin {
- void initialize(Map<String, String> commandLineOptions);
-
boolean canServeUri(String uri, File rootDir);
+ void initialize(Map<String, String> commandLineOptions);
+
NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, IHTTPSession session, File file, String mimeType);
}
diff --git a/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java b/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
index c1ef6e8..0fe5f4e 100644
--- a/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
+++ b/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
@@ -38,9 +38,9 @@ package fi.iki.elonen;
*/
public interface WebServerPluginInfo {
- String[] getMimeTypes();
-
String[] getIndexFilesForMimeType(String mime);
+ String[] getMimeTypes();
+
WebServerPlugin getWebServerPlugin(String mimeType);
}
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
index 8163c10..8fc38f7 100644
--- a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
+++ b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
@@ -54,133 +54,6 @@ import fi.iki.elonen.NanoWebSocketServer.WebSocketFrame.OpCode;
public abstract class NanoWebSocketServer extends NanoHTTPD {
- /**
- * logger to log to.
- */
- private static Logger LOG = Logger.getLogger(NanoWebSocketServer.class.getName());
-
- public static final String HEADER_UPGRADE = "upgrade";
-
- public static final String HEADER_UPGRADE_VALUE = "websocket";
-
- public static final String HEADER_CONNECTION = "connection";
-
- public static final String HEADER_CONNECTION_VALUE = "Upgrade";
-
- public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
-
- public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
-
- public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
-
- public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
-
- public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
-
- private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-
- public NanoWebSocketServer(int port) {
- super(port);
- }
-
- public NanoWebSocketServer(String hostname, int port) {
- super(hostname, port);
- }
-
- @Override
- public Response serve(final IHTTPSession session) {
- Map<String, String> headers = session.getHeaders();
- if (isWebsocketRequested(session)) {
- if (!HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(HEADER_WEBSOCKET_VERSION))) {
- return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Invalid Websocket-Version " + headers.get(HEADER_WEBSOCKET_VERSION));
- }
-
- if (!headers.containsKey(HEADER_WEBSOCKET_KEY)) {
- return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key");
- }
-
- WebSocket webSocket = openWebSocket(session);
- Response handshakeResponse = webSocket.getHandshakeResponse();
- try {
- handshakeResponse.addHeader(HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(HEADER_WEBSOCKET_KEY)));
- } catch (NoSuchAlgorithmException e) {
- return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "The SHA-1 Algorithm required for websockets is not available on the server.");
- }
-
- if (headers.containsKey(HEADER_WEBSOCKET_PROTOCOL)) {
- handshakeResponse.addHeader(HEADER_WEBSOCKET_PROTOCOL, headers.get(HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
- }
-
- return handshakeResponse;
- } else {
- return super.serve(session);
- }
- }
-
- protected boolean isWebsocketRequested(IHTTPSession session) {
- Map<String, String> headers = session.getHeaders();
- String upgrade = headers.get(HEADER_UPGRADE);
- boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
- boolean isUpgrade = HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
- return (isUpgrade && isCorrectConnection);
- }
-
- private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
- String connection = headers.get(HEADER_CONNECTION);
- return (connection != null && connection.toLowerCase().contains(HEADER_CONNECTION_VALUE.toLowerCase()));
- }
-
- public WebSocket openWebSocket(IHTTPSession handshake) {
- return new WebSocket(handshake);
- }
-
- public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
- MessageDigest md = MessageDigest.getInstance("SHA-1");
- String text = key + WEBSOCKET_KEY_MAGIC;
- md.update(text.getBytes(), 0, text.length());
- byte[] sha1hash = md.digest();
- return encodeBase64(sha1hash);
- }
-
- private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
-
- /**
- * Translates the specified byte array into Base64 string.
- * <p>
- * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8
- * hast java.util.Base64, I have this from stackoverflow:
- * http://stackoverflow.com/a/4265472
- * </p>
- *
- * @param buf
- * the byte array (not null)
- * @return the translated Base64 string (not null)
- */
- private static String encodeBase64(byte[] buf) {
- int size = buf.length;
- char[] ar = new char[((size + 2) / 3) * 4];
- int a = 0;
- int i = 0;
- while (i < size) {
- byte b0 = buf[i++];
- byte b1 = (i < size) ? buf[i++] : 0;
- byte b2 = (i < size) ? buf[i++] : 0;
-
- int mask = 0x3F;
- ar[a++] = ALPHABET[(b0 >> 2) & mask];
- ar[a++] = ALPHABET[((b0 << 4) | ((b1 & 0xFF) >> 4)) & mask];
- ar[a++] = ALPHABET[((b1 << 2) | ((b2 & 0xFF) >> 6)) & mask];
- ar[a++] = ALPHABET[b2 & mask];
- }
- switch (size % 3) {
- case 1:
- ar[--a] = '=';
- case 2:
- ar[--a] = '=';
- }
- return new String(ar);
- }
-
public static enum State {
UNCONNECTED,
CONNECTING,
@@ -191,13 +64,13 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
public class WebSocket {
- private InputStream in;
+ private final InputStream in;
private OutputStream out;
private WebSocketFrame.OpCode continuousOpCode = null;
- private List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
+ private final List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
private State state = State.UNCONNECTED;
@@ -208,9 +81,9 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
@Override
protected void send(OutputStream out) {
WebSocket.this.out = out;
- state = State.CONNECTING;
+ WebSocket.this.state = State.CONNECTING;
super.send(out);
- state = State.OPEN;
+ WebSocket.this.state = State.OPEN;
readWebsocket();
}
};
@@ -219,55 +92,50 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
this.handshakeRequest = handshakeRequest;
this.in = handshakeRequest.getInputStream();
- handshakeResponse.addHeader(HEADER_UPGRADE, HEADER_UPGRADE_VALUE);
- handshakeResponse.addHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
+ this.handshakeResponse.addHeader(NanoWebSocketServer.HEADER_UPGRADE, NanoWebSocketServer.HEADER_UPGRADE_VALUE);
+ this.handshakeResponse.addHeader(NanoWebSocketServer.HEADER_CONNECTION, NanoWebSocketServer.HEADER_CONNECTION_VALUE);
}
- public NanoHTTPD.IHTTPSession getHandshakeRequest() {
- return handshakeRequest;
+ public void close(CloseCode code, String reason) throws IOException {
+ State oldState = this.state;
+ this.state = State.CLOSING;
+ if (oldState == State.OPEN) {
+ sendFrame(new CloseFrame(code, reason));
+ } else {
+ doClose(code, reason, false);
+ }
}
- public NanoHTTPD.Response getHandshakeResponse() {
- return handshakeResponse;
+ private void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
+ if (this.state == State.CLOSED) {
+ return;
+ }
+ if (this.in != null) {
+ try {
+ this.in.close();
+ } catch (IOException e) {
+ NanoWebSocketServer.LOG.log(Level.FINE, "close failed", e);
+ }
+ }
+ if (this.out != null) {
+ try {
+ this.out.close();
+ } catch (IOException e) {
+ NanoWebSocketServer.LOG.log(Level.FINE, "close failed", e);
+ }
+ }
+ this.state = State.CLOSED;
+ onClose(this, code, reason, initiatedByRemote);
}
// --------------------------------IO--------------------------------------
- private void readWebsocket() {
- try {
- while (state == State.OPEN) {
- handleWebsocketFrame(WebSocketFrame.read(in));
- }
- } catch (CharacterCodingException e) {
- onException(this, e);
- doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
- } catch (IOException e) {
- onException(this, e);
- if (e instanceof WebSocketException) {
- doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
- }
- } finally {
- doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
- }
+ public NanoHTTPD.IHTTPSession getHandshakeRequest() {
+ return this.handshakeRequest;
}
- private void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
- onFrameReceived(frame);
- if (frame.getOpCode() == OpCode.Close) {
- handleCloseFrame(frame);
- } else if (frame.getOpCode() == OpCode.Ping) {
- sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
- } else if (frame.getOpCode() == OpCode.Pong) {
- onPong(this, frame);
- } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
- handleFrameFragment(frame);
- } else if (continuousOpCode != null) {
- throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
- } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
- onMessage(this, frame);
- } else {
- throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
- }
+ public NanoHTTPD.Response getHandshakeResponse() {
+ return this.handshakeResponse;
}
private void handleCloseFrame(WebSocketFrame frame) throws IOException {
@@ -277,13 +145,13 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
code = ((CloseFrame) frame).getCloseCode();
reason = ((CloseFrame) frame).getCloseReason();
}
- if (state == State.CLOSING) {
+ if (this.state == State.CLOSING) {
// Answer for my requested close
doClose(code, reason, false);
} else {
// Answer close request from other endpoint and close self
- State oldState = state;
- state = State.CLOSING;
+ State oldState = this.state;
+ this.state = State.CLOSING;
if (oldState == State.OPEN) {
sendFrame(new CloseFrame(code, reason));
}
@@ -294,63 +162,73 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
private void handleFrameFragment(WebSocketFrame frame) throws IOException {
if (frame.getOpCode() != OpCode.Continuation) {
// First
- if (continuousOpCode != null) {
+ if (this.continuousOpCode != null) {
throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
}
- continuousOpCode = frame.getOpCode();
- continuousFrames.clear();
- continuousFrames.add(frame);
+ this.continuousOpCode = frame.getOpCode();
+ this.continuousFrames.clear();
+ this.continuousFrames.add(frame);
} else if (frame.isFin()) {
// Last
- if (continuousOpCode == null) {
+ if (this.continuousOpCode == null) {
throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
}
- onMessage(this, new WebSocketFrame(continuousOpCode, continuousFrames));
- continuousOpCode = null;
- continuousFrames.clear();
- } else if (continuousOpCode == null) {
+ onMessage(this, new WebSocketFrame(this.continuousOpCode, this.continuousFrames));
+ this.continuousOpCode = null;
+ this.continuousFrames.clear();
+ } else if (this.continuousOpCode == null) {
// Unexpected
throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
} else {
// Intermediate
- continuousFrames.add(frame);
+ this.continuousFrames.add(frame);
}
}
- public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
- onSendFrame(frame);
- frame.write(out);
+ private void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
+ onFrameReceived(frame);
+ if (frame.getOpCode() == OpCode.Close) {
+ handleCloseFrame(frame);
+ } else if (frame.getOpCode() == OpCode.Ping) {
+ sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
+ } else if (frame.getOpCode() == OpCode.Pong) {
+ onPong(this, frame);
+ } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
+ handleFrameFragment(frame);
+ } else if (this.continuousOpCode != null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
+ } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
+ onMessage(this, frame);
+ } else {
+ throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
+ }
}
// --------------------------------Close-----------------------------------
- private void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
- if (state == State.CLOSED) {
- return;
- }
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- LOG.log(Level.FINE, "close failed", e);
- }
- }
- if (out != null) {
- try {
- out.close();
- } catch (IOException e) {
- LOG.log(Level.FINE, "close failed", e);
- }
- }
- state = State.CLOSED;
- onClose(this, code, reason, initiatedByRemote);
+ public void ping(byte[] payload) throws IOException {
+ sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
}
// --------------------------------Public
// Facade---------------------------
- public void ping(byte[] payload) throws IOException {
- sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
+ private void readWebsocket() {
+ try {
+ while (this.state == State.OPEN) {
+ handleWebsocketFrame(WebSocketFrame.read(this.in));
+ }
+ } catch (CharacterCodingException e) {
+ onException(this, e);
+ doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
+ } catch (IOException e) {
+ onException(this, e);
+ if (e instanceof WebSocketException) {
+ doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
+ }
+ } finally {
+ doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
+ }
}
public void send(byte[] payload) throws IOException {
@@ -361,19 +239,196 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
sendFrame(new WebSocketFrame(OpCode.Text, true, payload));
}
- public void close(CloseCode code, String reason) throws IOException {
- State oldState = state;
- state = State.CLOSING;
- if (oldState == State.OPEN) {
- sendFrame(new CloseFrame(code, reason));
- } else {
- doClose(code, reason, false);
- }
+ public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
+ onSendFrame(frame);
+ frame.write(this.out);
+ }
+ }
+
+ public static class WebSocketException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final CloseCode code;
+
+ private final String reason;
+
+ public WebSocketException(CloseCode code, String reason) {
+ this(code, reason, null);
+ }
+
+ public WebSocketException(CloseCode code, String reason, Exception cause) {
+ super(code + ": " + reason, cause);
+ this.code = code;
+ this.reason = reason;
+ }
+
+ public WebSocketException(Exception cause) {
+ this(CloseCode.InternalServerError, cause.toString(), cause);
+ }
+
+ public CloseCode getCode() {
+ return this.code;
+ }
+
+ public String getReason() {
+ return this.reason;
}
}
public static class WebSocketFrame {
+ public static enum CloseCode {
+ NormalClosure(1000),
+ GoingAway(1001),
+ ProtocolError(1002),
+ UnsupportedData(1003),
+ NoStatusRcvd(1005),
+ AbnormalClosure(1006),
+ InvalidFramePayloadData(1007),
+ PolicyViolation(1008),
+ MessageTooBig(1009),
+ MandatoryExt(1010),
+ InternalServerError(1011),
+ TLSHandshake(1015);
+
+ public static CloseCode find(int value) {
+ for (CloseCode code : values()) {
+ if (code.getValue() == value) {
+ return code;
+ }
+ }
+ return null;
+ }
+
+ private final int code;
+
+ private CloseCode(int code) {
+ this.code = code;
+ }
+
+ public int getValue() {
+ return this.code;
+ }
+ }
+
+ public static class CloseFrame extends WebSocketFrame {
+
+ private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
+ if (code != null) {
+ byte[] reasonBytes = text2Binary(closeReason);
+ byte[] payload = new byte[reasonBytes.length + 2];
+ payload[0] = (byte) (code.getValue() >> 8 & 0xFF);
+ payload[1] = (byte) (code.getValue() & 0xFF);
+ System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
+ return payload;
+ } else {
+ return new byte[0];
+ }
+ }
+
+ private CloseCode _closeCode;
+
+ private String _closeReason;
+
+ public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
+ super(OpCode.Close, true, generatePayload(code, closeReason));
+ }
+
+ private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
+ super(wrap);
+ assert wrap.getOpCode() == OpCode.Close;
+ if (wrap.getBinaryPayload().length >= 2) {
+ this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF);
+ this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
+ }
+ }
+
+ public CloseCode getCloseCode() {
+ return this._closeCode;
+ }
+
+ public String getCloseReason() {
+ return this._closeReason;
+ }
+ }
+
+ public static enum OpCode {
+ Continuation(0),
+ Text(1),
+ Binary(2),
+ Close(8),
+ Ping(9),
+ Pong(10);
+
+ public static OpCode find(byte value) {
+ for (OpCode opcode : values()) {
+ if (opcode.getValue() == value) {
+ return opcode;
+ }
+ }
+ return null;
+ }
+
+ private final byte code;
+
+ private OpCode(int code) {
+ this.code = (byte) code;
+ }
+
+ public byte getValue() {
+ return this.code;
+ }
+
+ public boolean isControlFrame() {
+ return this == Close || this == Ping || this == Pong;
+ }
+ }
+
+ public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
+
+ public static String binary2Text(byte[] payload) throws CharacterCodingException {
+ return new String(payload, WebSocketFrame.TEXT_CHARSET);
+ }
+
+ public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
+ return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET);
+ }
+
+ private static int checkedRead(int read) throws IOException {
+ if (read < 0) {
+ throw new EOFException();
+ }
+ return read;
+ }
+
+ public static WebSocketFrame read(InputStream in) throws IOException {
+ byte head = (byte) checkedRead(in.read());
+ boolean fin = (head & 0x80) != 0;
+ OpCode opCode = OpCode.find((byte) (head & 0x0F));
+ if ((head & 0x70) != 0) {
+ throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
+ }
+ if (opCode == null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
+ } else if (opCode.isControlFrame() && !fin) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
+ }
+
+ WebSocketFrame frame = new WebSocketFrame(opCode, fin);
+ frame.readPayloadInfo(in);
+ frame.readPayload(in);
+ if (frame.getOpCode() == OpCode.Close) {
+ return new CloseFrame(frame);
+ } else {
+ return frame;
+ }
+ }
+
+ public static byte[] text2Binary(String payload) throws CharacterCodingException {
+ return payload.getBytes(WebSocketFrame.TEXT_CHARSET);
+ }
+
private OpCode opCode;
private boolean fin;
@@ -382,6 +437,8 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
private byte[] payload;
+ // --------------------------------GETTERS---------------------------------
+
private transient int _payloadLength;
private transient String _payloadString;
@@ -391,13 +448,17 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
setFin(fin);
}
+ public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
+ this(opCode, fin, payload, null);
+ }
+
public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
this(opCode, fin);
setMaskingKey(maskingKey);
setBinaryPayload(payload);
}
- public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
+ public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
this(opCode, fin, payload, null);
}
@@ -407,17 +468,6 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
setTextPayload(payload);
}
- public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
- this(opCode, fin, payload, null);
- }
-
- public WebSocketFrame(WebSocketFrame clone) {
- setOpCode(clone.getOpCode());
- setFin(clone.isFin());
- setBinaryPayload(clone.getBinaryPayload());
- setMaskingKey(clone.getMaskingKey());
- }
-
public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException {
setOpCode(opCode);
setFin(true);
@@ -439,118 +489,108 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
setBinaryPayload(payload);
}
- // --------------------------------GETTERS---------------------------------
-
- public OpCode getOpCode() {
- return opCode;
- }
-
- public void setOpCode(OpCode opcode) {
- this.opCode = opcode;
- }
-
- public boolean isFin() {
- return fin;
- }
-
- public void setFin(boolean fin) {
- this.fin = fin;
+ public WebSocketFrame(WebSocketFrame clone) {
+ setOpCode(clone.getOpCode());
+ setFin(clone.isFin());
+ setBinaryPayload(clone.getBinaryPayload());
+ setMaskingKey(clone.getMaskingKey());
}
- public boolean isMasked() {
- return maskingKey != null && maskingKey.length == 4;
+ public byte[] getBinaryPayload() {
+ return this.payload;
}
public byte[] getMaskingKey() {
- return maskingKey;
- }
-
- public void setMaskingKey(byte[] maskingKey) {
- if (maskingKey != null && maskingKey.length != 4) {
- throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
- }
- this.maskingKey = maskingKey;
- }
-
- public void setUnmasked() {
- setMaskingKey(null);
+ return this.maskingKey;
}
- public byte[] getBinaryPayload() {
- return payload;
+ public OpCode getOpCode() {
+ return this.opCode;
}
- public void setBinaryPayload(byte[] payload) {
- this.payload = payload;
- this._payloadLength = payload.length;
- this._payloadString = null;
- }
+ // --------------------------------SERIALIZATION---------------------------
public String getTextPayload() {
- if (_payloadString == null) {
+ if (this._payloadString == null) {
try {
- _payloadString = binary2Text(getBinaryPayload());
+ this._payloadString = binary2Text(getBinaryPayload());
} catch (CharacterCodingException e) {
throw new RuntimeException("Undetected CharacterCodingException", e);
}
}
- return _payloadString;
+ return this._payloadString;
}
- public void setTextPayload(String payload) throws CharacterCodingException {
- this.payload = text2Binary(payload);
- this._payloadLength = payload.length();
- this._payloadString = payload;
+ public boolean isFin() {
+ return this.fin;
}
- // --------------------------------SERIALIZATION---------------------------
+ public boolean isMasked() {
+ return this.maskingKey != null && this.maskingKey.length == 4;
+ }
- public static WebSocketFrame read(InputStream in) throws IOException {
- byte head = (byte) checkedRead(in.read());
- boolean fin = ((head & 0x80) != 0);
- OpCode opCode = OpCode.find((byte) (head & 0x0F));
- if ((head & 0x70) != 0) {
- throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
+ private String payloadToString() {
+ if (this.payload == null) {
+ return "null";
+ } else {
+ final StringBuilder sb = new StringBuilder();
+ sb.append('[').append(this.payload.length).append("b] ");
+ if (getOpCode() == OpCode.Text) {
+ String text = getTextPayload();
+ if (text.length() > 100) {
+ sb.append(text.substring(0, 100)).append("...");
+ } else {
+ sb.append(text);
+ }
+ } else {
+ sb.append("0x");
+ for (int i = 0; i < Math.min(this.payload.length, 50); ++i) {
+ sb.append(Integer.toHexString(this.payload[i] & 0xFF));
+ }
+ if (this.payload.length > 50) {
+ sb.append("...");
+ }
+ }
+ return sb.toString();
}
- if (opCode == null) {
- throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
- } else if (opCode.isControlFrame() && !fin) {
- throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
+ }
+
+ private void readPayload(InputStream in) throws IOException {
+ this.payload = new byte[this._payloadLength];
+ int read = 0;
+ while (read < this._payloadLength) {
+ read += checkedRead(in.read(this.payload, read, this._payloadLength - read));
}
- WebSocketFrame frame = new WebSocketFrame(opCode, fin);
- frame.readPayloadInfo(in);
- frame.readPayload(in);
- if (frame.getOpCode() == OpCode.Close) {
- return new CloseFrame(frame);
- } else {
- return frame;
+ if (isMasked()) {
+ for (int i = 0; i < this.payload.length; i++) {
+ this.payload[i] ^= this.maskingKey[i % 4];
+ }
}
- }
- private static int checkedRead(int read) throws IOException {
- if (read < 0) {
- throw new EOFException();
+ // Test for Unicode errors
+ if (getOpCode() == OpCode.Text) {
+ this._payloadString = binary2Text(getBinaryPayload());
}
- return read;
}
+ // --------------------------------ENCODING--------------------------------
+
private void readPayloadInfo(InputStream in) throws IOException {
byte b = (byte) checkedRead(in.read());
- boolean masked = ((b & 0x80) != 0);
+ boolean masked = (b & 0x80) != 0;
- _payloadLength = (byte) (0x7F & b);
- if (_payloadLength == 126) {
+ this._payloadLength = (byte) (0x7F & b);
+ if (this._payloadLength == 126) {
// checkedRead must return int for this to work
- _payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
- if (_payloadLength < 126) {
+ this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
+ if (this._payloadLength < 126) {
throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
}
- } else if (_payloadLength == 127) {
+ } else if (this._payloadLength == 127) {
long _payloadLength =
- ((long) checkedRead(in.read())) << 56 | ((long) checkedRead(in.read())) << 48 | ((long) checkedRead(in.read())) << 40
- | ((long) checkedRead(in.read())) << 32 | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8
- | checkedRead(in.read());
+ (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32
+ | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
if (_payloadLength < 65536) {
throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
}
@@ -560,96 +600,55 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
this._payloadLength = (int) _payloadLength;
}
- if (opCode.isControlFrame()) {
- if (_payloadLength > 125) {
+ if (this.opCode.isControlFrame()) {
+ if (this._payloadLength > 125) {
throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
}
- if (opCode == OpCode.Close && _payloadLength == 1) {
+ if (this.opCode == OpCode.Close && this._payloadLength == 1) {
throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
}
}
if (masked) {
- maskingKey = new byte[4];
+ this.maskingKey = new byte[4];
int read = 0;
- while (read < maskingKey.length) {
- read += checkedRead(in.read(maskingKey, read, maskingKey.length - read));
+ while (read < this.maskingKey.length) {
+ read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read));
}
}
}
- private void readPayload(InputStream in) throws IOException {
- payload = new byte[_payloadLength];
- int read = 0;
- while (read < _payloadLength) {
- read += checkedRead(in.read(payload, read, _payloadLength - read));
- }
-
- if (isMasked()) {
- for (int i = 0; i < payload.length; i++) {
- payload[i] ^= maskingKey[i % 4];
- }
- }
-
- // Test for Unicode errors
- if (getOpCode() == OpCode.Text) {
- _payloadString = binary2Text(getBinaryPayload());
- }
+ public void setBinaryPayload(byte[] payload) {
+ this.payload = payload;
+ this._payloadLength = payload.length;
+ this._payloadString = null;
}
- public void write(OutputStream out) throws IOException {
- byte header = 0;
- if (fin) {
- header |= 0x80;
- }
- header |= opCode.getValue() & 0x0F;
- out.write(header);
-
- _payloadLength = getBinaryPayload().length;
- if (_payloadLength <= 125) {
- out.write(isMasked() ? 0x80 | (byte) _payloadLength : (byte) _payloadLength);
- } else if (_payloadLength <= 0xFFFF) {
- out.write(isMasked() ? 0xFE : 126);
- out.write(_payloadLength >>> 8);
- out.write(_payloadLength);
- } else {
- out.write(isMasked() ? 0xFF : 127);
- out.write(_payloadLength >>> 56 & 0); // integer only contains
- // 31 bit
- out.write(_payloadLength >>> 48 & 0);
- out.write(_payloadLength >>> 40 & 0);
- out.write(_payloadLength >>> 32 & 0);
- out.write(_payloadLength >>> 24);
- out.write(_payloadLength >>> 16);
- out.write(_payloadLength >>> 8);
- out.write(_payloadLength);
- }
+ public void setFin(boolean fin) {
+ this.fin = fin;
+ }
- if (isMasked()) {
- out.write(maskingKey);
- for (int i = 0; i < _payloadLength; i++) {
- out.write(getBinaryPayload()[i] ^ maskingKey[i % 4]);
- }
- } else {
- out.write(getBinaryPayload());
+ public void setMaskingKey(byte[] maskingKey) {
+ if (maskingKey != null && maskingKey.length != 4) {
+ throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
}
- out.flush();
+ this.maskingKey = maskingKey;
}
- // --------------------------------ENCODING--------------------------------
-
- public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
-
- public static String binary2Text(byte[] payload) throws CharacterCodingException {
- return new String(payload, TEXT_CHARSET);
+ public void setOpCode(OpCode opcode) {
+ this.opCode = opcode;
}
- public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
- return new String(payload, offset, length, TEXT_CHARSET);
+ public void setTextPayload(String payload) throws CharacterCodingException {
+ this.payload = text2Binary(payload);
+ this._payloadLength = payload.length();
+ this._payloadString = payload;
}
- public static byte[] text2Binary(String payload) throws CharacterCodingException {
- return payload.getBytes(TEXT_CHARSET);
+ // --------------------------------CONSTANTS-------------------------------
+
+ public void setUnmasked() {
+ setMaskingKey(null);
}
@Override
@@ -663,187 +662,192 @@ public abstract class NanoWebSocketServer extends NanoHTTPD {
return sb.toString();
}
- private String payloadToString() {
- if (payload == null)
- return "null";
- else {
- final StringBuilder sb = new StringBuilder();
- sb.append('[').append(payload.length).append("b] ");
- if (getOpCode() == OpCode.Text) {
- String text = getTextPayload();
- if (text.length() > 100)
- sb.append(text.substring(0, 100)).append("...");
- else
- sb.append(text);
- } else {
- sb.append("0x");
- for (int i = 0; i < Math.min(payload.length, 50); ++i)
- sb.append(Integer.toHexString((int) payload[i] & 0xFF));
- if (payload.length > 50)
- sb.append("...");
- }
- return sb.toString();
- }
- }
-
- // --------------------------------CONSTANTS-------------------------------
-
- public static enum OpCode {
- Continuation(0),
- Text(1),
- Binary(2),
- Close(8),
- Ping(9),
- Pong(10);
-
- private final byte code;
-
- private OpCode(int code) {
- this.code = (byte) code;
- }
+ // ------------------------------------------------------------------------
- public byte getValue() {
- return code;
+ public void write(OutputStream out) throws IOException {
+ byte header = 0;
+ if (this.fin) {
+ header |= 0x80;
}
+ header |= this.opCode.getValue() & 0x0F;
+ out.write(header);
- public boolean isControlFrame() {
- return this == Close || this == Ping || this == Pong;
+ this._payloadLength = getBinaryPayload().length;
+ if (this._payloadLength <= 125) {
+ out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength);
+ } else if (this._payloadLength <= 0xFFFF) {
+ out.write(isMasked() ? 0xFE : 126);
+ out.write(this._payloadLength >>> 8);
+ out.write(this._payloadLength);
+ } else {
+ out.write(isMasked() ? 0xFF : 127);
+ out.write(this._payloadLength >>> 56 & 0); // integer only
+ // contains
+ // 31 bit
+ out.write(this._payloadLength >>> 48 & 0);
+ out.write(this._payloadLength >>> 40 & 0);
+ out.write(this._payloadLength >>> 32 & 0);
+ out.write(this._payloadLength >>> 24);
+ out.write(this._payloadLength >>> 16);
+ out.write(this._payloadLength >>> 8);
+ out.write(this._payloadLength);
}
- public static OpCode find(byte value) {
- for (OpCode opcode : values()) {
- if (opcode.getValue() == value) {
- return opcode;
- }
+ if (isMasked()) {
+ out.write(this.maskingKey);
+ for (int i = 0; i < this._payloadLength; i++) {
+ out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]);
}
- return null;
+ } else {
+ out.write(getBinaryPayload());
}
+ out.flush();
}
+ }
- public static enum CloseCode {
- NormalClosure(1000),
- GoingAway(1001),
- ProtocolError(1002),
- UnsupportedData(1003),
- NoStatusRcvd(1005),
- AbnormalClosure(1006),
- InvalidFramePayloadData(1007),
- PolicyViolation(1008),
- MessageTooBig(1009),
- MandatoryExt(1010),
- InternalServerError(1011),
- TLSHandshake(1015);
+ /**
+ * logger to log to.
+ */
+ private static Logger LOG = Logger.getLogger(NanoWebSocketServer.class.getName());
- private final int code;
+ public static final String HEADER_UPGRADE = "upgrade";
- private CloseCode(int code) {
- this.code = code;
- }
+ public static final String HEADER_UPGRADE_VALUE = "websocket";
- public int getValue() {
- return code;
- }
+ public static final String HEADER_CONNECTION = "connection";
- public static CloseCode find(int value) {
- for (CloseCode code : values()) {
- if (code.getValue() == value) {
- return code;
- }
- }
- return null;
- }
- }
+ public static final String HEADER_CONNECTION_VALUE = "Upgrade";
- // ------------------------------------------------------------------------
+ public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
- public static class CloseFrame extends WebSocketFrame {
+ public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
- private CloseCode _closeCode;
+ public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
- private String _closeReason;
+ public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
- private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
- super(wrap);
- assert wrap.getOpCode() == OpCode.Close;
- if (wrap.getBinaryPayload().length >= 2) {
- _closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | (wrap.getBinaryPayload()[1] & 0xFF));
- _closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
- }
- }
+ public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
- public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
- super(OpCode.Close, true, generatePayload(code, closeReason));
- }
+ private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
- private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
- if (code != null) {
- byte[] reasonBytes = text2Binary(closeReason);
- byte[] payload = new byte[reasonBytes.length + 2];
- payload[0] = (byte) ((code.getValue() >> 8) & 0xFF);
- payload[1] = (byte) ((code.getValue()) & 0xFF);
- System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
- return payload;
- } else {
- return new byte[0];
- }
- }
+ private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
- public CloseCode getCloseCode() {
- return _closeCode;
- }
+ /**
+ * Translates the specified byte array into Base64 string.
+ * <p>
+ * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8
+ * hast java.util.Base64, I have this from stackoverflow:
+ * http://stackoverflow.com/a/4265472
+ * </p>
+ *
+ * @param buf
+ * the byte array (not null)
+ * @return the translated Base64 string (not null)
+ */
+ private static String encodeBase64(byte[] buf) {
+ int size = buf.length;
+ char[] ar = new char[(size + 2) / 3 * 4];
+ int a = 0;
+ int i = 0;
+ while (i < size) {
+ byte b0 = buf[i++];
+ byte b1 = i < size ? buf[i++] : 0;
+ byte b2 = i < size ? buf[i++] : 0;
- public String getCloseReason() {
- return _closeReason;
- }
+ int mask = 0x3F;
+ ar[a++] = NanoWebSocketServer.ALPHABET[b0 >> 2 & mask];
+ ar[a++] = NanoWebSocketServer.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask];
+ ar[a++] = NanoWebSocketServer.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask];
+ ar[a++] = NanoWebSocketServer.ALPHABET[b2 & mask];
+ }
+ switch (size % 3) {
+ case 1:
+ ar[--a] = '=';
+ case 2:
+ ar[--a] = '=';
}
+ return new String(ar);
}
- public static class WebSocketException extends IOException {
+ public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ String text = key + NanoWebSocketServer.WEBSOCKET_KEY_MAGIC;
+ md.update(text.getBytes(), 0, text.length());
+ byte[] sha1hash = md.digest();
+ return encodeBase64(sha1hash);
+ }
- private static final long serialVersionUID = 1L;
+ public NanoWebSocketServer(int port) {
+ super(port);
+ }
- private CloseCode code;
+ public NanoWebSocketServer(String hostname, int port) {
+ super(hostname, port);
+ }
- private String reason;
+ private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
+ String connection = headers.get(NanoWebSocketServer.HEADER_CONNECTION);
+ return connection != null && connection.toLowerCase().contains(NanoWebSocketServer.HEADER_CONNECTION_VALUE.toLowerCase());
+ }
- public WebSocketException(Exception cause) {
- this(CloseCode.InternalServerError, cause.toString(), cause);
- }
+ protected boolean isWebsocketRequested(IHTTPSession session) {
+ Map<String, String> headers = session.getHeaders();
+ String upgrade = headers.get(NanoWebSocketServer.HEADER_UPGRADE);
+ boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
+ boolean isUpgrade = NanoWebSocketServer.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
+ return isUpgrade && isCorrectConnection;
+ }
- public WebSocketException(CloseCode code, String reason) {
- this(code, reason, null);
- }
+ protected abstract void onClose(WebSocket webSocket, CloseCode code, String reason, boolean initiatedByRemote);
- public WebSocketException(CloseCode code, String reason, Exception cause) {
- super(code + ": " + reason, cause);
- this.code = code;
- this.reason = reason;
- }
+ protected abstract void onException(WebSocket webSocket, IOException e);
- public CloseCode getCode() {
- return code;
- }
+ // --------------------------------Listener--------------------------------
- public String getReason() {
- return reason;
- }
+ protected void onFrameReceived(WebSocketFrame webSocket) {
+ // only for debugging
}
- // --------------------------------Listener--------------------------------
+ protected abstract void onMessage(WebSocket webSocket, WebSocketFrame messageFrame);
protected abstract void onPong(WebSocket webSocket, WebSocketFrame pongFrame);
- protected abstract void onMessage(WebSocket webSocket, WebSocketFrame messageFrame);
+ public void onSendFrame(WebSocketFrame webSocket) {
+ // only for debugging
+ }
- protected abstract void onClose(WebSocket webSocket, CloseCode code, String reason, boolean initiatedByRemote);
+ public WebSocket openWebSocket(IHTTPSession handshake) {
+ return new WebSocket(handshake);
+ }
- protected abstract void onException(WebSocket webSocket, IOException e);
+ @Override
+ public Response serve(final IHTTPSession session) {
+ Map<String, String> headers = session.getHeaders();
+ if (isWebsocketRequested(session)) {
+ if (!NanoWebSocketServer.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_VERSION))) {
+ return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Invalid Websocket-Version "
+ + headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_VERSION));
+ }
- protected void onFrameReceived(WebSocketFrame webSocket) {
- // only for debugging
- }
+ if (!headers.containsKey(NanoWebSocketServer.HEADER_WEBSOCKET_KEY)) {
+ return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key");
+ }
- public void onSendFrame(WebSocketFrame webSocket) {
- // only for debugging
+ WebSocket webSocket = openWebSocket(session);
+ Response handshakeResponse = webSocket.getHandshakeResponse();
+ try {
+ handshakeResponse.addHeader(NanoWebSocketServer.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_KEY)));
+ } catch (NoSuchAlgorithmException e) {
+ return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "The SHA-1 Algorithm required for websockets is not available on the server.");
+ }
+
+ if (headers.containsKey(NanoWebSocketServer.HEADER_WEBSOCKET_PROTOCOL)) {
+ handshakeResponse.addHeader(NanoWebSocketServer.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
+ }
+
+ return handshakeResponse;
+ } else {
+ return super.serve(session);
+ }
}
}
diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
index 68c1a7e..b477bb8 100644
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
+++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
@@ -57,45 +57,45 @@ public class DebugWebSocketServer extends NanoWebSocketServer {
}
@Override
- protected void onPong(WebSocket socket, WebSocketFrame pongFrame) {
- if (debug) {
- System.out.println("P " + pongFrame);
+ protected void onClose(WebSocket socket, WebSocketFrame.CloseCode code, String reason, boolean initiatedByRemote) {
+ if (this.debug) {
+ System.out.println("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
+ + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
}
}
@Override
- protected void onMessage(WebSocket socket, WebSocketFrame messageFrame) {
- try {
- messageFrame.setUnmasked();
- socket.sendFrame(messageFrame);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ protected void onException(WebSocket socket, IOException e) {
+ DebugWebSocketServer.LOG.log(Level.SEVERE, "exception occured", e);
}
@Override
- protected void onClose(WebSocket socket, WebSocketFrame.CloseCode code, String reason, boolean initiatedByRemote) {
- if (debug) {
- System.out.println("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
- + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
+ protected void onFrameReceived(WebSocketFrame frame) {
+ if (this.debug) {
+ System.out.println("R " + frame);
}
}
@Override
- protected void onException(WebSocket socket, IOException e) {
- LOG.log(Level.SEVERE, "exception occured", e);
+ protected void onMessage(WebSocket socket, WebSocketFrame messageFrame) {
+ try {
+ messageFrame.setUnmasked();
+ socket.sendFrame(messageFrame);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
- protected void onFrameReceived(WebSocketFrame frame) {
- if (debug) {
- System.out.println("R " + frame);
+ protected void onPong(WebSocket socket, WebSocketFrame pongFrame) {
+ if (this.debug) {
+ System.out.println("P " + pongFrame);
}
}
@Override
public void onSendFrame(WebSocketFrame frame) {
- if (debug) {
+ if (this.debug) {
System.out.println("S " + frame);
}
}