aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md183
-rw-r--r--core/pom.xml2
-rw-r--r--core/src/main/java/fi/iki/elonen/NanoHTTPD.java38
-rw-r--r--pom.xml3
-rw-r--r--samples/pom.xml6
-rw-r--r--webserver/markdown-plugin/pom.xml8
-rw-r--r--webserver/pom.xml6
-rw-r--r--websocket/pom.xml93
-rw-r--r--websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java121
-rw-r--r--websocket/src/main/java/fi/iki/elonen/WebSocket.java203
-rw-r--r--websocket/src/main/java/fi/iki/elonen/WebSocketException.java32
-rw-r--r--websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java430
-rw-r--r--websocket/src/test/java/fi/iki/elonen/DebugWebSocket.java61
-rw-r--r--websocket/src/test/java/fi/iki/elonen/DebugWebSocketServer.java19
-rw-r--r--websocket/src/test/java/fi/iki/elonen/EchoSocketSample.java20
-rw-r--r--websocket/src/test/resources/echo-test.html58
17 files changed, 1082 insertions, 202 deletions
diff --git a/.gitignore b/.gitignore
index f46b4cf..a257787 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
out
+build
target
*.iml
diff --git a/README.md b/README.md
index afb6004..60b2578 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,6 @@
*NanoHttpd* has been released under a Modified BSD licence.
-## Current major development efforts
-
-Waffle.io Issue Tracking: [![Stories in Ready](https://badge.waffle.io/NanoHttpd/nanohttpd.png?label=ready)](https://waffle.io/NanoHttpd/nanohttpd)
-
-*Core*
-* Please take a look at the new "ssl-support" branch containing submitted code adding SSL support to NanoHttpd. It's a great new feature that needs all eyes to polish in preparation for a release, making sure it works on all platforms.
-
-*Webserver*
-* Internal architecture support URL rewriting. Serving "index.*" files now utilizes the feature. Capability needs to be extended to read and apply rewrite rules at runtime.
-* Plugin support - plugins that transform the source (eg PHP, Markdown) can now support caching generate files. Add caching to the markdown plugin as an example.
-
## Core Features
* Only one Java file, providing HTTP 1.1 support.
* 2 "flavors" - one at "current" standards and one strictly Java 1.1 compatible.
@@ -34,6 +23,9 @@ Waffle.io Issue Tracking: [![Stories in Ready](https://badge.waffle.io/NanoHttpd
* Temp file usage and threading model are easily cutomized.
* Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.
+## Websocket Support
+* Tested on Firefox, Chrome and IE.
+
## Webserver Features
* Supports both dynamic content and file serving.
* Default code serves files and shows all HTTP parameters and headers.
@@ -46,171 +38,4 @@ Waffle.io Issue Tracking: [![Stories in Ready](https://badge.waffle.io/NanoHttpd
* Contains a built-in list of most common mime types.
* Runtime extension support (extensions that serve particular mime types) - example extension that serves Markdown formatted files. Simply including an extension JAR in the webserver classpath is enough for the extension to be loaded.
-## How is the project managed?
-
-The project is managed with a "fork and pull-request" pattern.
-
-If you want to contribute, fork this repo and submit a pull-request of your changes when you're ready.
-
-Anyone can create Issues, and pull requests should be tied back to an issue describing the purpose of the submitted code.
-
-## The Tests
-
-In an ideal world for a bug fix: write a test of your own that fails as a result of the bug being present. Then fix the bug so that your unit-test passes. The test will now be a guard against the bug ever coming back.
-
-Similarly for enhancements, exercise your code with tests.
-
-Whatever else happens, if you make changes to the code, the unit & integration test suite (under ```core/src/main/test/```) should all continue to function. Pull requests with broken tests will be rejected.
-
-## Where can I find the original (Java1.1) NanoHttpd?
-
-The original (Java 1.1 project) and the Java 6 project merged in early 2013 to pool resources
-around "NanoHttpd" as a whole, regardless of flavor. Development of the Java 1.1 version continues
-as a permanent branch ("nanohttpd-for-java1.1") in the main http://github.com/NanoHttpd/nanohttpd repository.
-
-## How do I use nanohttpd?
-
-Firstly take a look at the "samples" sub-module. The sample code illustrates using NanoHttpd in various ways.
-
-Secondly, you can run the standalone *NanoHttpd Webserver*.
-
-Or, create your own class that extends `NanoHTTPD` and overrides one of the two flavors of the `serve()` method. For example:
-
-```java
-package fi.iki.elonen.debug;
-
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.ServerRunner;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class DebugServer extends NanoHTTPD {
- public DebugServer() {
- super(8080);
- }
-
- public static void main(String[] args) {
- ServerRunner.run(DebugServer.class);
- }
-
- @Override public Response serve(IHTTPSession session) {
- Map<String, List<String>> decodedQueryParameters =
- decodeParameters(session.getQueryParameterString());
-
- StringBuilder sb = new StringBuilder();
- sb.append("<html>");
- sb.append("<head><title>Debug Server</title></head>");
- sb.append("<body>");
- sb.append("<h1>Debug Server</h1>");
-
- sb.append("<p><blockquote><b>URI</b> = ").append(
- String.valueOf(session.getUri())).append("<br />");
-
- sb.append("<b>Method</b> = ").append(
- String.valueOf(session.getMethod())).append("</blockquote></p>");
-
- sb.append("<h3>Headers</h3><p><blockquote>").
- append(toString(session.getHeaders())).append("</blockquote></p>");
-
- sb.append("<h3>Parms</h3><p><blockquote>").
- append(toString(session.getParms())).append("</blockquote></p>");
-
- sb.append("<h3>Parms (multi values?)</h3><p><blockquote>").
- append(toString(decodedQueryParameters)).append("</blockquote></p>");
-
- try {
- Map<String, String> files = new HashMap<String, String>();
- session.parseBody(files);
- sb.append("<h3>Files</h3><p><blockquote>").
- append(toString(files)).append("</blockquote></p>");
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- sb.append("</body>");
- sb.append("</html>");
- return new Response(sb.toString());
- }
-
- private String toString(Map<String, ? extends Object> map) {
- if (map.size() == 0) {
- return "";
- }
- return unsortedList(map);
- }
-
- private String unsortedList(Map<String, ? extends Object> map) {
- StringBuilder sb = new StringBuilder();
- sb.append("<ul>");
- for (Map.Entry entry : map.entrySet()) {
- listItem(sb, entry);
- }
- sb.append("</ul>");
- return sb.toString();
- }
-
- private void listItem(StringBuilder sb, Map.Entry entry) {
- sb.append("<li><code><b>").append(entry.getKey()).
- append("</b> = ").append(entry.getValue()).append("</code></li>");
- }
-}
-```
-
-## Why fork the original repo?
-
-The Java 6 version of *nanohttpd* was born when we realized that embedding Jetty inside our
-Android application was going to inflate the size without bringing along features that we
-were going to need. The search for a smaller more streamlined HTTP server lead us
-to *nanohttpd* as the project had started with exactly the same goals, but we wanted to
-clear up the old code - move from Java 1.1, run _static code analysis_ tools and cleanup
-the findings and pull out sample/test code from the source.
-
-In the words of the original founder of the project
-> I couldn't find a small enough, embeddable and easily modifiable HTTP server
-> that I could just copy and paste into my other Java projects. Every one of them
-> consisted of dozens of .java files and/or jars, usually with - from my point
-> of view - "overkill features" like servlet support, web administration,
-> configuration files, logging etc.
-
-Since that time we fixed a number of bugs, moved the build to _maven_ and pulled out
-the samples from the runtime JAR to further slim it down.
-
-The two projects pooled resources in early 2013, merging code-bases, to better support the
-user base and reduce confusion over why _two_ NanoHttpd projects existed.
-
-http://nanohttpd.com - went live July 1st, 2013.
-
-## Version History (Java 6+ version)
-* 2.0.5 (2013-12-12) : Cleanup and stability fixes.
-* 2.0.4 (2013-09-15) : Added basic cookie support, experimental SSL support and runtime extensions.
-* 2.0.3 (2013-06-17) : Implemented 'Connection: keep-alive', (tested against latest Mozilla Firefox).
-* 2.0.2 (2013-06-06) : Polish for the webserver, and fixed a bug causing stack-traces on Samsung Phones.
-* 2.0.1 (2013-05-27) : Non-English UTF-8 decoding support for URLS/Filenames
-* 2.0.0 (2013-05-21) : Released - announced on FreeCode.com
-* (2013-05-20) : Test suite looks complete.
-* (2013-05-05) : Web server pulled out of samples and promoted to top-level project
-* (2013-03-09) : Work on test suite begins - the push for release 2.0.0 begins!
-* (2013-01-04) : Initial commit on "uplift" fork
-
-## Version History (Java 1.1 version)
-
-* 1.27 (2013-04-01): Merged several bug fixes from github forks
-* 1.26 (2013-03-27): fixed an off-by one bug
-* 1.25 (2012-02-12): rudimetary PUT support, buffer size now configurable, support for etag "if-none-match" check, log output stream now configurable
-* 1.24 (2011-08-04): etags + video mime types (for HTML5 video streaming)
-* 1.23 (2011-08-02): better support for partial requests
-* 1.22 (2011-07-21): support for custom www root dir
-* 1.21 (2011-01-03): minor bug fixes
-* 1.2 (2010-12-31): file upload (by Konstantinos Togias) and some small bug fixes
-* 1.14 (2010-08-20): added a stop() function
-* 1.13 (2010-06-27): fixed a wrong case in 'range' header
-* 1.12 (2010-01-07): fixed a null ptr exception
-* 1.11 (2008-04-21): fixed a double URI decoding (caused problems when there was a percent-coded percent)
-* 1.10 (2007-02-09): improved browser compatibility by forcing headers lowercase; fixed a POST method over-read bug
-* 1.05 (2006-03-30): honor Content-Length header; support for clients that leave TCP connection open; better MIME support for symlinked files
-* 1.02 (2005-07-08): fixed a stream read starvation bug
-* 1.01 (2003-04-03): first published version
-
-Thank you to everyone who has reported bugs and suggested fixes.
+*Thank you to everyone who has reported bugs and suggested fixes.*
diff --git a/core/pom.xml b/core/pom.xml
index bc46337..e82d9d0 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -4,7 +4,7 @@
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<packaging>jar</packaging>
<name>NanoHttpd-Core</name>
diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
index a5a6232..b1d59c3 100644
--- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
+++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -549,7 +549,7 @@ public abstract class NanoHTTPD {
/**
* HTTP status code after processing, e.g. "200 OK", HTTP_OK
*/
- private Status status;
+ private IStatus status;
/**
* MIME type of content, e.g. "text/html"
*/
@@ -581,7 +581,7 @@ public abstract class NanoHTTPD {
/**
* Basic constructor.
*/
- public Response(Status status, String mimeType, InputStream data) {
+ public Response(IStatus status, String mimeType, InputStream data) {
this.status = status;
this.mimeType = mimeType;
this.data = data;
@@ -590,7 +590,7 @@ public abstract class NanoHTTPD {
/**
* Convenience method that makes an InputStream out of given text.
*/
- public Response(Status status, String mimeType, String txt) {
+ public Response(IStatus status, String mimeType, String txt) {
this.status = status;
this.mimeType = mimeType;
try {
@@ -610,7 +610,7 @@ public abstract class NanoHTTPD {
/**
* Sends given response to the socket.
*/
- private void send(OutputStream outputStream) {
+ 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"));
@@ -637,7 +637,7 @@ public abstract class NanoHTTPD {
}
}
- pw.print("Connection: keep-alive\r\n");
+ sendConnectionHeaderIfNotAlreadyPresent(pw, header);
if (requestMethod != Method.HEAD && chunkedTransfer) {
sendAsChunked(outputStream, pw);
@@ -651,6 +651,16 @@ public abstract class NanoHTTPD {
}
}
+ protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
+ boolean connectionAlreadySent = false;
+ for (String headerName : header.keySet()) {
+ connectionAlreadySent |= headerName.equalsIgnoreCase("connection");
+ }
+ if (!connectionAlreadySent) {
+ pw.print("Connection: keep-alive\r\n");
+ }
+ }
+
private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
pw.print("Transfer-Encoding: chunked\r\n");
pw.print("\r\n");
@@ -689,7 +699,7 @@ public abstract class NanoHTTPD {
}
}
- public Status getStatus() {
+ public IStatus getStatus() {
return status;
}
@@ -725,11 +735,16 @@ public abstract class NanoHTTPD {
this.chunkedTransfer = chunkedTransfer;
}
+ public interface IStatus {
+ int getRequestStatus();
+ String getDescription();
+ }
+
/**
* Some HTTP response status codes
*/
- public enum Status {
- OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301,
+ 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");
@@ -741,10 +756,12 @@ public abstract class NanoHTTPD {
this.description = description;
}
+ @Override
public int getRequestStatus() {
return this.requestStatus;
}
+ @Override
public String getDescription() {
return "" + this.requestStatus + " " + description;
}
@@ -1201,7 +1218,7 @@ public abstract class NanoHTTPD {
dest.write(src.slice());
path = tempFile.getName();
} catch (Exception e) { // Catch exception if any
- System.err.println("Error: " + e.getMessage());
+ throw new Error(e); // we won't recover, so throw an error
} finally {
safeClose(fileOutputStream);
}
@@ -1214,9 +1231,8 @@ public abstract class NanoHTTPD {
TempFile tempFile = tempFileManager.createTempFile();
return new RandomAccessFile(tempFile.getName(), "rw");
} catch (Exception e) {
- System.err.println("Error: " + e.getMessage());
+ throw new Error(e); // we won't recover, so throw an error
}
- return null;
}
/**
diff --git a/pom.xml b/pom.xml
index 25b175d..6dd6be2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-project</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<packaging>pom</packaging>
<name>NanoHttpd-Project</name>
@@ -14,6 +14,7 @@
<module>samples</module>
<module>webserver</module>
<module>webserver/markdown-plugin</module>
+ <module>websocket</module>
</modules>
<build>
diff --git a/samples/pom.xml b/samples/pom.xml
index be080ab..a32b07b 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -4,7 +4,7 @@
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-samples</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<packaging>jar</packaging>
<name>NanoHttpd-Samples</name>
@@ -14,12 +14,12 @@
<dependency>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
</dependency>
<dependency>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-webserver</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
</dependency>
</dependencies>
diff --git a/webserver/markdown-plugin/pom.xml b/webserver/markdown-plugin/pom.xml
index 4c901d9..278eb28 100644
--- a/webserver/markdown-plugin/pom.xml
+++ b/webserver/markdown-plugin/pom.xml
@@ -1,10 +1,10 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-webserver-markdown-plugin</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<packaging>jar</packaging>
<name>NanoHttpd-Webserver-Markdown-Plugin</name>
@@ -14,13 +14,13 @@
<dependency>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-webserver</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/webserver/pom.xml b/webserver/pom.xml
index c983d4a..cfc0bd3 100644
--- a/webserver/pom.xml
+++ b/webserver/pom.xml
@@ -1,10 +1,10 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd-webserver</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
<packaging>jar</packaging>
<name>NanoHttpd-Webserver</name>
@@ -14,7 +14,7 @@
<dependency>
<groupId>fi.iki.elonen</groupId>
<artifactId>nanohttpd</artifactId>
- <version>2.0.5</version>
+ <version>2.1.0</version>
</dependency>
</dependencies>
diff --git a/websocket/pom.xml b/websocket/pom.xml
new file mode 100644
index 0000000..ca24ea2
--- /dev/null
+++ b/websocket/pom.xml
@@ -0,0 +1,93 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>fi.iki.elonen</groupId>
+ <artifactId>nanohttpd-websocket</artifactId>
+ <version>2.1.0</version>
+ <packaging>jar</packaging>
+
+ <name>NanoHttpd-Websocket</name>
+ <url>https://github.com/NanoHttpd/nanohttpd</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>fi.iki.elonen</groupId>
+ <artifactId>nanohttpd</artifactId>
+ <version>2.1.0</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.jvnet.wagon-svn</groupId>
+ <artifactId>wagon-svn</artifactId>
+ <version>1.8</version>
+ </extension>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-ftp</artifactId>
+ <version>1.0-alpha-6</version>
+ </extension>
+ </extensions>
+
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>2.2.1</version>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-release-plugin</artifactId>
+ <version>2.4</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <version>2.9</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.1</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.2-beta-5</version>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ <archive>
+ <manifest>
+ <mainClass>fi.iki.elonen.NanoWebSocketServer</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
new file mode 100644
index 0000000..7a5e588
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
@@ -0,0 +1,121 @@
+package fi.iki.elonen;
+
+import fi.iki.elonen.NanoHTTPD;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+public abstract class NanoWebSocketServer extends NanoHTTPD {
+ 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";
+
+ public 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_UPGRADE_VALUE.equalsIgnoreCase(headers.get(HEADER_UPGRADE))
+ || !isWebSocketConnectionHeader(session.getHeaders())) {
+ return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Invalid Websocket handshake");
+ }
+ 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);
+ try {
+ webSocket.getHandshakeResponse().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)) {
+ webSocket.getHandshakeResponse().addHeader(HEADER_WEBSOCKET_PROTOCOL, headers.get(HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
+ }
+ return webSocket.getHandshakeResponse();
+ } else {
+ return super.serve(session);
+ }
+ }
+
+ protected abstract WebSocket openWebSocket(IHTTPSession handshake);
+
+ 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 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);
+ }
+}
+
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocket.java b/websocket/src/main/java/fi/iki/elonen/WebSocket.java
new file mode 100644
index 0000000..84bf259
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/WebSocket.java
@@ -0,0 +1,203 @@
+package fi.iki.elonen;
+
+import fi.iki.elonen.WebSocketFrame.CloseCode;
+import fi.iki.elonen.WebSocketFrame.CloseFrame;
+import fi.iki.elonen.WebSocketFrame.OpCode;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.CharacterCodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+public abstract class WebSocket {
+ protected final InputStream in;
+ protected /*final*/ OutputStream out;
+
+ protected WebSocketFrame.OpCode continuousOpCode = null;
+ protected List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
+
+ protected State state = State.UNCONNECTED;
+
+ public static enum State {
+ UNCONNECTED, CONNECTING, OPEN, CLOSING, CLOSED
+ }
+
+ protected final NanoHTTPD.IHTTPSession handshakeRequest;
+ protected final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null) {
+ @Override
+ protected void send(OutputStream out) {
+ WebSocket.this.out = out;
+ state = State.CONNECTING;
+ super.send(out);
+ state = State.OPEN;
+ readWebsocket();
+ }
+ };
+
+ public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {
+ this.handshakeRequest = handshakeRequest;
+ this.in = handshakeRequest.getInputStream();
+
+ handshakeResponse.addHeader(NanoWebSocketServer.HEADER_UPGRADE, NanoWebSocketServer.HEADER_UPGRADE_VALUE);
+ handshakeResponse.addHeader(NanoWebSocketServer.HEADER_CONNECTION, NanoWebSocketServer.HEADER_CONNECTION_VALUE);
+ }
+
+ // --------------------------------IO--------------------------------------
+
+ protected void readWebsocket() {
+ try {
+ while (state == State.OPEN) {
+ handleWebsocketFrame(WebSocketFrame.read(in));
+ }
+ } catch (CharacterCodingException e) {
+ onException(e);
+ doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
+ } catch (IOException e) {
+ onException(e);
+ if (e instanceof WebSocketException) {
+ doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
+ }
+ } finally {
+ doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
+ }
+ }
+
+ protected void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
+ 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(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(frame);
+ } else {
+ throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
+ }
+ }
+
+ protected void handleCloseFrame(WebSocketFrame frame) throws IOException {
+ CloseCode code = CloseCode.NormalClosure;
+ String reason = "";
+ if (frame instanceof CloseFrame) {
+ code = ((CloseFrame) frame).getCloseCode();
+ reason = ((CloseFrame) frame).getCloseReason();
+ }
+ if (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;
+ if (oldState == State.OPEN) {
+ sendFrame(new CloseFrame(code, reason));
+ }
+ doClose(code, reason, true);
+ }
+ }
+
+ protected void handleFrameFragment(WebSocketFrame frame) throws IOException {
+ if (frame.getOpCode() != OpCode.Continuation) {
+ //First
+ if (continuousOpCode != null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
+ }
+ continuousOpCode = frame.getOpCode();
+ continuousFrames.clear();
+ continuousFrames.add(frame);
+ } else if (frame.isFin()) {
+ //Last
+ if (continuousOpCode == null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+ }
+ onMessage(new WebSocketFrame(continuousOpCode, continuousFrames));
+ continuousOpCode = null;
+ continuousFrames.clear();
+ } else if (continuousOpCode == null) {
+ //Unexpected
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+ } else {
+ //Intermediate
+ continuousFrames.add(frame);
+ }
+ }
+
+ public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
+ frame.write(out);
+ }
+
+ // --------------------------------Close-----------------------------------
+
+ protected void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
+ if (state == State.CLOSED) {
+ return;
+ }
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ state = State.CLOSED;
+ onClose(code, reason, initiatedByRemote);
+ }
+
+ // --------------------------------Listener--------------------------------
+
+ protected abstract void onPong(WebSocketFrame pongFrame);
+
+ protected abstract void onMessage(WebSocketFrame messageFrame);
+
+ protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);
+
+ protected abstract void onException(IOException e);
+
+ // --------------------------------Public Facade---------------------------
+
+ public void ping(byte[] payload) throws IOException {
+ sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
+ }
+
+ public void send(byte[] payload) throws IOException {
+ sendFrame(new WebSocketFrame(OpCode.Binary, true, payload));
+ }
+
+ public void send(String payload) throws IOException {
+ 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);
+ }
+ }
+
+ // --------------------------------Getters---------------------------------
+
+ public NanoHTTPD.IHTTPSession getHandshakeRequest() {
+ return handshakeRequest;
+ }
+
+ public NanoHTTPD.Response getHandshakeResponse() {
+ return handshakeResponse;
+ }
+}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketException.java b/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
new file mode 100644
index 0000000..31cb6c8
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
@@ -0,0 +1,32 @@
+package fi.iki.elonen;
+
+import fi.iki.elonen.WebSocketFrame.CloseCode;
+
+import java.io.IOException;
+
+public class WebSocketException extends IOException {
+ private CloseCode code;
+ private String reason;
+
+ public WebSocketException(Exception cause) {
+ this(CloseCode.InternalServerError, cause.toString(), cause);
+ }
+
+ 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 CloseCode getCode() {
+ return code;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java b/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java
new file mode 100644
index 0000000..0e209df
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java
@@ -0,0 +1,430 @@
+package fi.iki.elonen;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.util.Arrays;
+import java.util.List;
+
+public class WebSocketFrame {
+ private OpCode opCode;
+ private boolean fin;
+ private byte[] maskingKey;
+
+ private byte[] payload;
+
+ private transient int _payloadLength;
+ private transient String _payloadString;
+
+ private WebSocketFrame(OpCode opCode, boolean fin) {
+ setOpCode(opCode);
+ setFin(fin);
+ }
+
+ 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) {
+ this(opCode, fin, payload, null);
+ }
+
+ public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException {
+ this(opCode, fin);
+ setMaskingKey(maskingKey);
+ 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);
+
+ long _payloadLength = 0;
+ for (WebSocketFrame inter : fragments) {
+ _payloadLength += inter.getBinaryPayload().length;
+ }
+ if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
+ throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
+ }
+ this._payloadLength = (int) _payloadLength;
+ byte[] payload = new byte[this._payloadLength];
+ int offset = 0;
+ for (WebSocketFrame inter : fragments) {
+ System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length);
+ offset += inter.getBinaryPayload().length;
+ }
+ 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 boolean isMasked() {
+ return maskingKey != null && maskingKey.length == 4;
+ }
+
+ 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);
+ }
+
+ public byte[] getBinaryPayload() {
+ return payload;
+ }
+
+ public void setBinaryPayload(byte[] payload) {
+ this.payload = payload;
+ this._payloadLength = payload.length;
+ this._payloadString = null;
+ }
+
+ public String getTextPayload() {
+ if (_payloadString == null) {
+ try {
+ _payloadString = binary2Text(getBinaryPayload());
+ } catch (CharacterCodingException e) {
+ throw new RuntimeException("Undetected CharacterCodingException", e);
+ }
+ }
+ return _payloadString;
+ }
+
+ public void setTextPayload(String payload) throws CharacterCodingException {
+ this.payload = text2Binary(payload);
+ this._payloadLength = payload.length();
+ this._payloadString = payload;
+ }
+
+ // --------------------------------SERIALIZATION---------------------------
+
+ 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;
+ }
+ }
+
+ private static int checkedRead(int read) throws IOException {
+ if (read < 0) {
+ throw new EOFException();
+ }
+ //System.out.println(Integer.toBinaryString(read) + "/" + read + "/" + Integer.toHexString(read));
+ return read;
+ }
+
+
+ private void readPayloadInfo(InputStream in) throws IOException {
+ byte b = (byte) checkedRead(in.read());
+ boolean masked = ((b & 0x80) != 0);
+
+ _payloadLength = (byte) (0x7F & b);
+ if (_payloadLength == 126) {
+ // checkedRead must return int for this to work
+ _payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
+ if (_payloadLength < 126) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
+ }
+ } else if (_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());
+ if (_payloadLength < 65536) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
+ }
+ if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
+ throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
+ }
+ this._payloadLength = (int) _payloadLength;
+ }
+
+ if (opCode.isControlFrame()) {
+ if (_payloadLength > 125) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
+ }
+ if (opCode == OpCode.Close && _payloadLength == 1) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
+ }
+ }
+
+ if (masked) {
+ maskingKey = new byte[4];
+ int read = 0;
+ while (read < maskingKey.length) {
+ read += checkedRead(in.read(maskingKey, read, 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 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);
+ }
+
+
+ if (isMasked()) {
+ out.write(maskingKey);
+ for (int i = 0; i < _payloadLength; i++) {
+ out.write(getBinaryPayload()[i] ^ maskingKey[i % 4]);
+ }
+ } else {
+ out.write(getBinaryPayload());
+ }
+ out.flush();
+ }
+
+ // --------------------------------ENCODING--------------------------------
+
+ public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
+ public static final CharsetDecoder TEXT_DECODER = TEXT_CHARSET.newDecoder();
+ public static final CharsetEncoder TEXT_ENCODER = TEXT_CHARSET.newEncoder();
+
+
+ public static String binary2Text(byte[] payload) throws CharacterCodingException {
+ return TEXT_DECODER.decode(ByteBuffer.wrap(payload)).toString();
+ }
+
+ public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
+ return TEXT_DECODER.decode(ByteBuffer.wrap(payload, offset, length)).toString();
+ }
+
+ public static byte[] text2Binary(String payload) throws CharacterCodingException {
+ return TEXT_ENCODER.encode(CharBuffer.wrap(payload)).array();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("WS[");
+ sb.append(getOpCode());
+ sb.append(", ").append(isFin() ? "fin" : "inter");
+ sb.append(", ").append(isMasked() ? "masked" : "unmasked");
+ sb.append(", ").append(payloadToString());
+ sb.append(']');
+ return sb.toString();
+ }
+
+ protected 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 boolean isControlFrame() {
+ return this == Close || this == Ping || this == Pong;
+ }
+
+ public static OpCode find(byte value) {
+ for (OpCode opcode : values()) {
+ if (opcode.getValue() == value) {
+ return opcode;
+ }
+ }
+ return null;
+ }
+ }
+
+ 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);
+
+ private final int code;
+
+ private CloseCode(int code) {
+ this.code = code;
+ }
+
+ public int getValue() {
+ return code;
+ }
+
+ public static CloseCode find(int value) {
+ for (CloseCode code : values()) {
+ if (code.getValue() == value) {
+ return code;
+ }
+ }
+ return null;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+
+ public static class CloseFrame extends WebSocketFrame {
+ private CloseCode _closeCode;
+ private String _closeReason;
+
+ 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 CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
+ super(OpCode.Close, true, generatePayload(code, closeReason));
+ }
+
+ 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];
+ }
+ }
+
+ protected String payloadToString() {
+ return (_closeCode != null ? _closeCode : "UnknownCloseCode[" + _closeCode + "]") + (_closeReason != null && !_closeReason.isEmpty() ? ": " + _closeReason : "");
+ }
+
+ public CloseCode getCloseCode() {
+ return _closeCode;
+ }
+
+ public String getCloseReason() {
+ return _closeReason;
+ }
+ }
+}
diff --git a/websocket/src/test/java/fi/iki/elonen/DebugWebSocket.java b/websocket/src/test/java/fi/iki/elonen/DebugWebSocket.java
new file mode 100644
index 0000000..e6c66ad
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/DebugWebSocket.java
@@ -0,0 +1,61 @@
+package fi.iki.elonen;
+
+import java.io.IOException;
+
+/**
+* @author Paul S. Hawke (paul.hawke@gmail.com)
+* On: 4/23/14 at 10:34 PM
+*/
+class DebugWebSocket extends WebSocket {
+ private final boolean DEBUG;
+
+ public DebugWebSocket(NanoHTTPD.IHTTPSession handshake, boolean debug) {
+ super(handshake);
+ DEBUG = debug;
+ }
+
+ @Override
+ protected void onPong(WebSocketFrame pongFrame) {
+ if (DEBUG) {
+ System.out.println("P " + pongFrame);
+ }
+ }
+
+ @Override
+ protected void onMessage(WebSocketFrame messageFrame) {
+ try {
+ messageFrame.setUnmasked();
+ sendFrame(messageFrame);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected void onClose(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 : ""));
+ }
+ }
+
+ @Override
+ protected void onException(IOException e) {
+ e.printStackTrace();
+ }
+
+ @Override
+ protected void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
+ if (DEBUG) {
+ System.out.println("R " + frame);
+ }
+ super.handleWebsocketFrame(frame);
+ }
+
+ @Override
+ public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
+ if (DEBUG) {
+ System.out.println("S " + frame);
+ }
+ super.sendFrame(frame);
+ }
+}
diff --git a/websocket/src/test/java/fi/iki/elonen/DebugWebSocketServer.java b/websocket/src/test/java/fi/iki/elonen/DebugWebSocketServer.java
new file mode 100644
index 0000000..49fbe4d
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/DebugWebSocketServer.java
@@ -0,0 +1,19 @@
+package fi.iki.elonen;
+
+/**
+* @author Paul S. Hawke (paul.hawke@gmail.com)
+* On: 4/23/14 at 10:31 PM
+*/
+class DebugWebSocketServer extends NanoWebSocketServer {
+ private final boolean debug;
+
+ public DebugWebSocketServer(int port, boolean debug) {
+ super(port);
+ this.debug = debug;
+ }
+
+ @Override
+ protected WebSocket openWebSocket(IHTTPSession handshake) {
+ return new DebugWebSocket(handshake, debug);
+ }
+}
diff --git a/websocket/src/test/java/fi/iki/elonen/EchoSocketSample.java b/websocket/src/test/java/fi/iki/elonen/EchoSocketSample.java
new file mode 100644
index 0000000..091dfc0
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/EchoSocketSample.java
@@ -0,0 +1,20 @@
+package fi.iki.elonen;
+
+import java.io.IOException;
+
+public class EchoSocketSample {
+ public static void main(String[] args) throws IOException {
+ final boolean debugMode = args.length >= 2 && args[1].toLowerCase().equals("-d");
+ NanoWebSocketServer ws = new DebugWebSocketServer(Integer.parseInt(args[0]), debugMode);
+ ws.start();
+ System.out.println("Server started, hit Enter to stop.\n");
+ try {
+ System.in.read();
+ } catch (IOException ignored) {
+ }
+ ws.stop();
+ System.out.println("Server stopped.\n");
+ }
+
+}
+
diff --git a/websocket/src/test/resources/echo-test.html b/websocket/src/test/resources/echo-test.html
new file mode 100644
index 0000000..4b60b80
--- /dev/null
+++ b/websocket/src/test/resources/echo-test.html
@@ -0,0 +1,58 @@
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title>WebSocket Test</title>
+ <script language="javascript" type="text/javascript">
+ var wsUri = "ws://localhost:9090/";
+ var output;
+ function init() {
+ output = document.getElementById("output");
+ testWebSocket();
+ }
+ function testWebSocket() {
+ websocket = new WebSocket(wsUri);
+ websocket.onopen = function (evt) {
+ onOpen(evt)
+ };
+ websocket.onclose = function (evt) {
+ onClose(evt)
+ };
+ websocket.onmessage = function (evt) {
+ onMessage(evt)
+ };
+ websocket.onerror = function (evt) {
+ onError(evt)
+ };
+ }
+ function onOpen(evt) {
+ writeToScreen("CONNECTED");
+ doSend("WebSocket rocks");
+ }
+ function onClose(evt) {
+ writeToScreen("DISCONNECTED");
+ }
+ function onMessage(evt) {
+ writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data + '</span>');
+ websocket.close();
+ }
+ function onError(evt) {
+ writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
+ }
+ function doSend(message) {
+ writeToScreen("SENT: " + message);
+ websocket.send(message);
+ }
+ function writeToScreen(message) {
+ var pre = document.createElement("p");
+ pre.style.wordWrap = "break-word";
+ pre.innerHTML = message;
+ output.appendChild(pre);
+ }
+ window.addEventListener("load", init, false); </script>
+</head>
+<body>
+<h2>WebSocket Test</h2>
+
+<div id="output"></div>
+</body>
+</html> \ No newline at end of file