diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 183 | ||||
-rw-r--r-- | core/pom.xml | 2 | ||||
-rw-r--r-- | core/src/main/java/fi/iki/elonen/NanoHTTPD.java | 38 | ||||
-rw-r--r-- | pom.xml | 3 | ||||
-rw-r--r-- | samples/pom.xml | 6 | ||||
-rw-r--r-- | webserver/markdown-plugin/pom.xml | 8 | ||||
-rw-r--r-- | webserver/pom.xml | 6 | ||||
-rw-r--r-- | websocket/pom.xml | 93 | ||||
-rw-r--r-- | websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java | 121 | ||||
-rw-r--r-- | websocket/src/main/java/fi/iki/elonen/WebSocket.java | 203 | ||||
-rw-r--r-- | websocket/src/main/java/fi/iki/elonen/WebSocketException.java | 32 | ||||
-rw-r--r-- | websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java | 430 | ||||
-rw-r--r-- | websocket/src/test/java/fi/iki/elonen/DebugWebSocket.java | 61 | ||||
-rw-r--r-- | websocket/src/test/java/fi/iki/elonen/DebugWebSocketServer.java | 19 | ||||
-rw-r--r-- | websocket/src/test/java/fi/iki/elonen/EchoSocketSample.java | 20 | ||||
-rw-r--r-- | websocket/src/test/resources/echo-test.html | 58 |
17 files changed, 1082 insertions, 202 deletions
@@ -1,4 +1,5 @@ out +build target *.iml @@ -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; } /** @@ -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 |