aboutsummaryrefslogtreecommitdiff
path: root/websocket
diff options
context:
space:
mode:
Diffstat (limited to 'websocket')
-rw-r--r--websocket/.gitignore2
-rw-r--r--websocket/pom.xml170
-rw-r--r--websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java7
-rw-r--r--websocket/src/main/java/fi/iki/elonen/NanoWSD.java876
-rw-r--r--websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java39
-rw-r--r--websocket/src/main/java/fi/iki/elonen/WebSocket.java207
-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/main/java/fi/iki/elonen/WebSocketResponseHandler.java118
-rw-r--r--websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java67
-rw-r--r--websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java116
-rw-r--r--websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java39
-rw-r--r--websocket/src/site/site.xml41
-rw-r--r--websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java41
-rw-r--r--websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java181
-rw-r--r--websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java102
-rw-r--r--websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java104
-rw-r--r--websocket/src/test/resources/echo-test.html32
18 files changed, 1483 insertions, 1121 deletions
diff --git a/websocket/.gitignore b/websocket/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/websocket/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/websocket/pom.xml b/websocket/pom.xml
index f9d9703..4f66283 100644
--- a/websocket/pom.xml
+++ b/websocket/pom.xml
@@ -1,104 +1,68 @@
-<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>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.8.2</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-all</artifactId>
- <version>1.9.5</version>
- </dependency>
- <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 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>
+ <parent>
+ <groupId>org.nanohttpd</groupId>
+ <artifactId>nanohttpd-project</artifactId>
+ <version>2.2.0</version>
+ </parent>
+ <artifactId>nanohttpd-websocket</artifactId>
+ <packaging>jar</packaging>
+ <name>NanoHttpd-Websocket</name>
+ <description>nanohttpd-websocket is a very low profile websocket server based on nanohttpd.</description>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-jar</id>
+ <configuration>
+ <excludes>
+ <exclude>**/samples/**</exclude>
+ </excludes>
+ </configuration>
+ </execution>
+ <execution>
+ <id>echo-jar</id>
+ <phase>package</phase>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ <configuration>
+ <classifier>echo</classifier>
+ <archive>
+ <manifest>
+ <addClasspath>true</addClasspath>
+ <mainClass>fi.iki.elonen.samples.echo.EchoSocketSample</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>nanohttpd</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <version>1.9.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-client</artifactId>
+ <version>9.3.0.M2</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <properties>
+ <minimal.coverage>0.67</minimal.coverage>
+ </properties>
</project>
diff --git a/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java b/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java
deleted file mode 100644
index 3e38cd2..0000000
--- a/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package fi.iki.elonen;
-
-import fi.iki.elonen.NanoHTTPD.IHTTPSession;
-
-public interface IWebSocketFactory {
- WebSocket openWebSocket(IHTTPSession handshake);
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWSD.java b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java
new file mode 100644
index 0000000..b0c72f5
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java
@@ -0,0 +1,876 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode;
+
+public abstract class NanoWSD extends NanoHTTPD {
+
+ public static enum State {
+ UNCONNECTED,
+ CONNECTING,
+ OPEN,
+ CLOSING,
+ CLOSED
+ }
+
+ public static abstract class WebSocket {
+
+ private final InputStream in;
+
+ private OutputStream out;
+
+ private WebSocketFrame.OpCode continuousOpCode = null;
+
+ private final List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
+
+ private State state = State.UNCONNECTED;
+
+ private final NanoHTTPD.IHTTPSession handshakeRequest;
+
+ private final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null, 0) {
+
+ @Override
+ protected void send(OutputStream out) {
+ WebSocket.this.out = out;
+ WebSocket.this.state = State.CONNECTING;
+ super.send(out);
+ WebSocket.this.state = State.OPEN;
+ WebSocket.this.onOpen();
+ readWebsocket();
+ }
+ };
+
+ public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {
+ this.handshakeRequest = handshakeRequest;
+ this.in = handshakeRequest.getInputStream();
+
+ this.handshakeResponse.addHeader(NanoWSD.HEADER_UPGRADE, NanoWSD.HEADER_UPGRADE_VALUE);
+ this.handshakeResponse.addHeader(NanoWSD.HEADER_CONNECTION, NanoWSD.HEADER_CONNECTION_VALUE);
+ }
+
+ public boolean isOpen() {
+ return state == State.OPEN;
+ }
+
+ protected abstract void onOpen();
+
+ protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);
+
+ protected abstract void onMessage(WebSocketFrame message);
+
+ protected abstract void onPong(WebSocketFrame pong);
+
+ protected abstract void onException(IOException exception);
+
+ /**
+ * Debug method. <b>Do not Override unless for debug purposes!</b>
+ *
+ * @param frame
+ * The received WebSocket Frame.
+ */
+ protected void debugFrameReceived(WebSocketFrame frame) {
+ }
+
+ /**
+ * Debug method. <b>Do not Override unless for debug purposes!</b><br>
+ * This method is called before actually sending the frame.
+ *
+ * @param frame
+ * The sent WebSocket Frame.
+ */
+ protected void debugFrameSent(WebSocketFrame frame) {
+ }
+
+ public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException {
+ State oldState = this.state;
+ this.state = State.CLOSING;
+ if (oldState == State.OPEN) {
+ sendFrame(new CloseFrame(code, reason));
+ } else {
+ doClose(code, reason, initiatedByRemote);
+ }
+ }
+
+ private void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
+ if (this.state == State.CLOSED) {
+ return;
+ }
+ if (this.in != null) {
+ try {
+ this.in.close();
+ } catch (IOException e) {
+ NanoWSD.LOG.log(Level.FINE, "close failed", e);
+ }
+ }
+ if (this.out != null) {
+ try {
+ this.out.close();
+ } catch (IOException e) {
+ NanoWSD.LOG.log(Level.FINE, "close failed", e);
+ }
+ }
+ this.state = State.CLOSED;
+ onClose(code, reason, initiatedByRemote);
+ }
+
+ // --------------------------------IO--------------------------------------
+
+ public NanoHTTPD.IHTTPSession getHandshakeRequest() {
+ return this.handshakeRequest;
+ }
+
+ public NanoHTTPD.Response getHandshakeResponse() {
+ return this.handshakeResponse;
+ }
+
+ private 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 (this.state == State.CLOSING) {
+ // Answer for my requested close
+ doClose(code, reason, false);
+ } else {
+ close(code, reason, true);
+ }
+ }
+
+ private void handleFrameFragment(WebSocketFrame frame) throws IOException {
+ if (frame.getOpCode() != OpCode.Continuation) {
+ // First
+ if (this.continuousOpCode != null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
+ }
+ this.continuousOpCode = frame.getOpCode();
+ this.continuousFrames.clear();
+ this.continuousFrames.add(frame);
+ } else if (frame.isFin()) {
+ // Last
+ if (this.continuousOpCode == null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+ }
+ onMessage(new WebSocketFrame(this.continuousOpCode, this.continuousFrames));
+ this.continuousOpCode = null;
+ this.continuousFrames.clear();
+ } else if (this.continuousOpCode == null) {
+ // Unexpected
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+ } else {
+ // Intermediate
+ this.continuousFrames.add(frame);
+ }
+ }
+
+ private void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
+ debugFrameReceived(frame);
+ if (frame.getOpCode() == OpCode.Close) {
+ handleCloseFrame(frame);
+ } else if (frame.getOpCode() == OpCode.Ping) {
+ sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
+ } else if (frame.getOpCode() == OpCode.Pong) {
+ onPong(frame);
+ } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
+ handleFrameFragment(frame);
+ } else if (this.continuousOpCode != null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
+ } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
+ onMessage(frame);
+ } else {
+ throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
+ }
+ }
+
+ // --------------------------------Close-----------------------------------
+
+ public void ping(byte[] payload) throws IOException {
+ sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
+ }
+
+ // --------------------------------Public
+ // Facade---------------------------
+
+ private void readWebsocket() {
+ try {
+ while (this.state == State.OPEN) {
+ handleWebsocketFrame(WebSocketFrame.read(this.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);
+ }
+ }
+
+ 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 synchronized void sendFrame(WebSocketFrame frame) throws IOException {
+ debugFrameSent(frame);
+ frame.write(this.out);
+ }
+ }
+
+ public static class WebSocketException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final CloseCode code;
+
+ private final String reason;
+
+ public WebSocketException(CloseCode code, String reason) {
+ this(code, reason, null);
+ }
+
+ public WebSocketException(CloseCode code, String reason, Exception cause) {
+ super(code + ": " + reason, cause);
+ this.code = code;
+ this.reason = reason;
+ }
+
+ public WebSocketException(Exception cause) {
+ this(CloseCode.InternalServerError, cause.toString(), cause);
+ }
+
+ public CloseCode getCode() {
+ return this.code;
+ }
+
+ public String getReason() {
+ return this.reason;
+ }
+ }
+
+ public static class WebSocketFrame {
+
+ public static enum CloseCode {
+ NormalClosure(1000),
+ GoingAway(1001),
+ ProtocolError(1002),
+ UnsupportedData(1003),
+ NoStatusRcvd(1005),
+ AbnormalClosure(1006),
+ InvalidFramePayloadData(1007),
+ PolicyViolation(1008),
+ MessageTooBig(1009),
+ MandatoryExt(1010),
+ InternalServerError(1011),
+ TLSHandshake(1015);
+
+ public static CloseCode find(int value) {
+ for (CloseCode code : values()) {
+ if (code.getValue() == value) {
+ return code;
+ }
+ }
+ return null;
+ }
+
+ private final int code;
+
+ private CloseCode(int code) {
+ this.code = code;
+ }
+
+ public int getValue() {
+ return this.code;
+ }
+ }
+
+ public static class CloseFrame extends WebSocketFrame {
+
+ private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
+ if (code != null) {
+ byte[] reasonBytes = text2Binary(closeReason);
+ byte[] payload = new byte[reasonBytes.length + 2];
+ payload[0] = (byte) (code.getValue() >> 8 & 0xFF);
+ payload[1] = (byte) (code.getValue() & 0xFF);
+ System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
+ return payload;
+ } else {
+ return new byte[0];
+ }
+ }
+
+ private CloseCode _closeCode;
+
+ private String _closeReason;
+
+ public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
+ super(OpCode.Close, true, generatePayload(code, closeReason));
+ }
+
+ private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
+ super(wrap);
+ assert wrap.getOpCode() == OpCode.Close;
+ if (wrap.getBinaryPayload().length >= 2) {
+ this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF);
+ this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
+ }
+ }
+
+ public CloseCode getCloseCode() {
+ return this._closeCode;
+ }
+
+ public String getCloseReason() {
+ return this._closeReason;
+ }
+ }
+
+ public static enum OpCode {
+ Continuation(0),
+ Text(1),
+ Binary(2),
+ Close(8),
+ Ping(9),
+ Pong(10);
+
+ public static OpCode find(byte value) {
+ for (OpCode opcode : values()) {
+ if (opcode.getValue() == value) {
+ return opcode;
+ }
+ }
+ return null;
+ }
+
+ private final byte code;
+
+ private OpCode(int code) {
+ this.code = (byte) code;
+ }
+
+ public byte getValue() {
+ return this.code;
+ }
+
+ public boolean isControlFrame() {
+ return this == Close || this == Ping || this == Pong;
+ }
+ }
+
+ public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
+
+ public static String binary2Text(byte[] payload) throws CharacterCodingException {
+ return new String(payload, WebSocketFrame.TEXT_CHARSET);
+ }
+
+ public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
+ return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET);
+ }
+
+ private static int checkedRead(int read) throws IOException {
+ if (read < 0) {
+ throw new EOFException();
+ }
+ return read;
+ }
+
+ public static WebSocketFrame read(InputStream in) throws IOException {
+ byte head = (byte) checkedRead(in.read());
+ boolean fin = (head & 0x80) != 0;
+ OpCode opCode = OpCode.find((byte) (head & 0x0F));
+ if ((head & 0x70) != 0) {
+ throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
+ }
+ if (opCode == null) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
+ } else if (opCode.isControlFrame() && !fin) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
+ }
+
+ WebSocketFrame frame = new WebSocketFrame(opCode, fin);
+ frame.readPayloadInfo(in);
+ frame.readPayload(in);
+ if (frame.getOpCode() == OpCode.Close) {
+ return new CloseFrame(frame);
+ } else {
+ return frame;
+ }
+ }
+
+ public static byte[] text2Binary(String payload) throws CharacterCodingException {
+ return payload.getBytes(WebSocketFrame.TEXT_CHARSET);
+ }
+
+ private OpCode opCode;
+
+ private boolean fin;
+
+ private byte[] maskingKey;
+
+ private byte[] payload;
+
+ // --------------------------------GETTERS---------------------------------
+
+ 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) {
+ this(opCode, fin, payload, null);
+ }
+
+ public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
+ this(opCode, fin);
+ setMaskingKey(maskingKey);
+ setBinaryPayload(payload);
+ }
+
+ public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
+ 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, 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);
+ }
+
+ public WebSocketFrame(WebSocketFrame clone) {
+ setOpCode(clone.getOpCode());
+ setFin(clone.isFin());
+ setBinaryPayload(clone.getBinaryPayload());
+ setMaskingKey(clone.getMaskingKey());
+ }
+
+ public byte[] getBinaryPayload() {
+ return this.payload;
+ }
+
+ public byte[] getMaskingKey() {
+ return this.maskingKey;
+ }
+
+ public OpCode getOpCode() {
+ return this.opCode;
+ }
+
+ // --------------------------------SERIALIZATION---------------------------
+
+ public String getTextPayload() {
+ if (this._payloadString == null) {
+ try {
+ this._payloadString = binary2Text(getBinaryPayload());
+ } catch (CharacterCodingException e) {
+ throw new RuntimeException("Undetected CharacterCodingException", e);
+ }
+ }
+ return this._payloadString;
+ }
+
+ public boolean isFin() {
+ return this.fin;
+ }
+
+ public boolean isMasked() {
+ return this.maskingKey != null && this.maskingKey.length == 4;
+ }
+
+ private String payloadToString() {
+ if (this.payload == null) {
+ return "null";
+ } else {
+ final StringBuilder sb = new StringBuilder();
+ sb.append('[').append(this.payload.length).append("b] ");
+ if (getOpCode() == OpCode.Text) {
+ String text = getTextPayload();
+ if (text.length() > 100) {
+ sb.append(text.substring(0, 100)).append("...");
+ } else {
+ sb.append(text);
+ }
+ } else {
+ sb.append("0x");
+ for (int i = 0; i < Math.min(this.payload.length, 50); ++i) {
+ sb.append(Integer.toHexString(this.payload[i] & 0xFF));
+ }
+ if (this.payload.length > 50) {
+ sb.append("...");
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ private void readPayload(InputStream in) throws IOException {
+ this.payload = new byte[this._payloadLength];
+ int read = 0;
+ while (read < this._payloadLength) {
+ read += checkedRead(in.read(this.payload, read, this._payloadLength - read));
+ }
+
+ if (isMasked()) {
+ for (int i = 0; i < this.payload.length; i++) {
+ this.payload[i] ^= this.maskingKey[i % 4];
+ }
+ }
+
+ // Test for Unicode errors
+ if (getOpCode() == OpCode.Text) {
+ this._payloadString = binary2Text(getBinaryPayload());
+ }
+ }
+
+ // --------------------------------ENCODING--------------------------------
+
+ private void readPayloadInfo(InputStream in) throws IOException {
+ byte b = (byte) checkedRead(in.read());
+ boolean masked = (b & 0x80) != 0;
+
+ this._payloadLength = (byte) (0x7F & b);
+ if (this._payloadLength == 126) {
+ // checkedRead must return int for this to work
+ this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
+ if (this._payloadLength < 126) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
+ }
+ } else if (this._payloadLength == 127) {
+ long _payloadLength =
+ (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32
+ | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
+ 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 (this.opCode.isControlFrame()) {
+ if (this._payloadLength > 125) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
+ }
+ if (this.opCode == OpCode.Close && this._payloadLength == 1) {
+ throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
+ }
+ }
+
+ if (masked) {
+ this.maskingKey = new byte[4];
+ int read = 0;
+ while (read < this.maskingKey.length) {
+ read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read));
+ }
+ }
+ }
+
+ public void setBinaryPayload(byte[] payload) {
+ this.payload = payload;
+ this._payloadLength = payload.length;
+ this._payloadString = null;
+ }
+
+ public void setFin(boolean fin) {
+ this.fin = fin;
+ }
+
+ 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 setOpCode(OpCode opcode) {
+ this.opCode = opcode;
+ }
+
+ public void setTextPayload(String payload) throws CharacterCodingException {
+ this.payload = text2Binary(payload);
+ this._payloadLength = payload.length();
+ this._payloadString = payload;
+ }
+
+ // --------------------------------CONSTANTS-------------------------------
+
+ public void setUnmasked() {
+ setMaskingKey(null);
+ }
+
+ @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();
+ }
+
+ // ------------------------------------------------------------------------
+
+ public void write(OutputStream out) throws IOException {
+ byte header = 0;
+ if (this.fin) {
+ header |= 0x80;
+ }
+ header |= this.opCode.getValue() & 0x0F;
+ out.write(header);
+
+ this._payloadLength = getBinaryPayload().length;
+ if (this._payloadLength <= 125) {
+ out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength);
+ } else if (this._payloadLength <= 0xFFFF) {
+ out.write(isMasked() ? 0xFE : 126);
+ out.write(this._payloadLength >>> 8);
+ out.write(this._payloadLength);
+ } else {
+ out.write(isMasked() ? 0xFF : 127);
+ out.write(this._payloadLength >>> 56 & 0); // integer only
+ // contains
+ // 31 bit
+ out.write(this._payloadLength >>> 48 & 0);
+ out.write(this._payloadLength >>> 40 & 0);
+ out.write(this._payloadLength >>> 32 & 0);
+ out.write(this._payloadLength >>> 24);
+ out.write(this._payloadLength >>> 16);
+ out.write(this._payloadLength >>> 8);
+ out.write(this._payloadLength);
+ }
+
+ if (isMasked()) {
+ out.write(this.maskingKey);
+ for (int i = 0; i < this._payloadLength; i++) {
+ out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]);
+ }
+ } else {
+ out.write(getBinaryPayload());
+ }
+ out.flush();
+ }
+ }
+
+ /**
+ * logger to log to.
+ */
+ private static final Logger LOG = Logger.getLogger(NanoWSD.class.getName());
+
+ public static final String HEADER_UPGRADE = "upgrade";
+
+ public static final String HEADER_UPGRADE_VALUE = "websocket";
+
+ public static final String HEADER_CONNECTION = "connection";
+
+ public static final String HEADER_CONNECTION_VALUE = "Upgrade";
+
+ public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
+
+ public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
+
+ public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
+
+ public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
+
+ public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
+
+ private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+ 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++] = NanoWSD.ALPHABET[b0 >> 2 & mask];
+ ar[a++] = NanoWSD.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask];
+ ar[a++] = NanoWSD.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask];
+ ar[a++] = NanoWSD.ALPHABET[b2 & mask];
+ }
+ switch (size % 3) {
+ case 1:
+ ar[--a] = '=';
+ case 2:
+ ar[--a] = '=';
+ }
+ return new String(ar);
+ }
+
+ public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ String text = key + NanoWSD.WEBSOCKET_KEY_MAGIC;
+ md.update(text.getBytes(), 0, text.length());
+ byte[] sha1hash = md.digest();
+ return encodeBase64(sha1hash);
+ }
+
+ public NanoWSD(int port) {
+ super(port);
+ }
+
+ public NanoWSD(String hostname, int port) {
+ super(hostname, port);
+ }
+
+ private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
+ String connection = headers.get(NanoWSD.HEADER_CONNECTION);
+ return connection != null && connection.toLowerCase().contains(NanoWSD.HEADER_CONNECTION_VALUE.toLowerCase());
+ }
+
+ protected boolean isWebsocketRequested(IHTTPSession session) {
+ Map<String, String> headers = session.getHeaders();
+ String upgrade = headers.get(NanoWSD.HEADER_UPGRADE);
+ boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
+ boolean isUpgrade = NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
+ return isUpgrade && isCorrectConnection;
+ }
+
+ // --------------------------------Listener--------------------------------
+
+ protected abstract WebSocket openWebSocket(IHTTPSession handshake);
+
+ @Override
+ public Response serve(final IHTTPSession session) {
+ Map<String, String> headers = session.getHeaders();
+ if (isWebsocketRequested(session)) {
+ if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) {
+ return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
+ "Invalid Websocket-Version " + headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION));
+ }
+
+ if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) {
+ return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key");
+ }
+
+ WebSocket webSocket = openWebSocket(session);
+ Response handshakeResponse = webSocket.getHandshakeResponse();
+ try {
+ handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWSD.HEADER_WEBSOCKET_KEY)));
+ } catch (NoSuchAlgorithmException e) {
+ return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
+ "The SHA-1 Algorithm required for websockets is not available on the server.");
+ }
+
+ if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) {
+ handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
+ }
+
+ return handshakeResponse;
+ } else {
+ return serveHttp(session);
+ }
+ }
+
+ protected Response serveHttp(final IHTTPSession session) {
+ return super.serve(session);
+ }
+
+ /**
+ * not all websockets implementations accept gzip compression.
+ */
+ @Override
+ protected boolean useGzipWhenAccepted(Response r) {
+ return false;
+ }
+}
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
deleted file mode 100644
index 0712371..0000000
--- a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package fi.iki.elonen;
-
-
-public class NanoWebSocketServer extends NanoHTTPD implements IWebSocketFactory {
- public static final String MISSING_FACTORY_MESSAGE = "You must either override this method or supply a WebSocketFactory in the constructor";
-
- private final WebSocketResponseHandler responseHandler;
-
- public NanoWebSocketServer(int port) {
- super(port);
- responseHandler = new WebSocketResponseHandler(this);
- }
-
- public NanoWebSocketServer(String hostname, int port) {
- super(hostname, port);
- responseHandler = new WebSocketResponseHandler(this);
- }
-
- public NanoWebSocketServer(int port, IWebSocketFactory webSocketFactory) {
- super(port);
- responseHandler = new WebSocketResponseHandler(webSocketFactory);
- }
-
- public NanoWebSocketServer(String hostname, int port, IWebSocketFactory webSocketFactory) {
- super(hostname, port);
- responseHandler = new WebSocketResponseHandler(webSocketFactory);
- }
-
- @Override
- public Response serve(IHTTPSession session) {
- Response candidate = responseHandler.serve(session);
- return candidate == null ? super.serve(session) : candidate;
- }
-
- public WebSocket openWebSocket(IHTTPSession handshake) {
- throw new Error(MISSING_FACTORY_MESSAGE);
- }
-}
-
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocket.java b/websocket/src/main/java/fi/iki/elonen/WebSocket.java
deleted file mode 100644
index 22b07fb..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocket.java
+++ /dev/null
@@ -1,207 +0,0 @@
-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 {
- public static enum State {
- UNCONNECTED, CONNECTING, OPEN, CLOSING, CLOSED
- }
-
- protected InputStream in;
-
- protected OutputStream out;
-
- protected WebSocketFrame.OpCode continuousOpCode = null;
-
- protected List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
-
- protected State state = State.UNCONNECTED;
-
- 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(WebSocketResponseHandler.HEADER_UPGRADE,
- WebSocketResponseHandler.HEADER_UPGRADE_VALUE);
- handshakeResponse.addHeader(WebSocketResponseHandler.HEADER_CONNECTION,
- WebSocketResponseHandler.HEADER_CONNECTION_VALUE);
- }
-
- public NanoHTTPD.IHTTPSession getHandshakeRequest() {
- return handshakeRequest;
- }
-
- public NanoHTTPD.Response getHandshakeResponse() {
- return handshakeResponse;
- }
-
- // --------------------------------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);
- }
- }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketException.java b/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
deleted file mode 100644
index 31cb6c8..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 0e209df..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java
+++ /dev/null
@@ -1,430 +0,0 @@
-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/main/java/fi/iki/elonen/WebSocketResponseHandler.java b/websocket/src/main/java/fi/iki/elonen/WebSocketResponseHandler.java
deleted file mode 100644
index 5c042af..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketResponseHandler.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package fi.iki.elonen;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Map;
-
-import fi.iki.elonen.NanoHTTPD.IHTTPSession;
-import fi.iki.elonen.NanoHTTPD.Response;
-
-public class WebSocketResponseHandler {
- 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";
-
- private final IWebSocketFactory webSocketFactory;
-
- public WebSocketResponseHandler(IWebSocketFactory webSocketFactory) {
- this.webSocketFactory = webSocketFactory;
- }
-
- public Response serve(final IHTTPSession session) {
- Map<String, String> headers = session.getHeaders();
- if (isWebsocketRequested(session)) {
- if (!HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(HEADER_WEBSOCKET_VERSION))) {
- return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
- "Invalid Websocket-Version " + headers.get(HEADER_WEBSOCKET_VERSION));
- }
-
- if (!headers.containsKey(HEADER_WEBSOCKET_KEY)) {
- return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
- "Missing Websocket-Key");
- }
-
- WebSocket webSocket = webSocketFactory.openWebSocket(session);
- Response handshakeResponse = webSocket.getHandshakeResponse();
- try {
- handshakeResponse.addHeader(HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(HEADER_WEBSOCKET_KEY)));
- } catch (NoSuchAlgorithmException e) {
- return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
- "The SHA-1 Algorithm required for websockets is not available on the server.");
- }
-
- if (headers.containsKey(HEADER_WEBSOCKET_PROTOCOL)) {
- handshakeResponse.addHeader(HEADER_WEBSOCKET_PROTOCOL, headers.get(HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
- }
-
- return handshakeResponse;
- } else {
- return null;
- }
- }
-
- 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/samples/echo/DebugWebSocket.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java
deleted file mode 100644
index badd7ee..0000000
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package fi.iki.elonen.samples.echo;
-
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.WebSocket;
-import fi.iki.elonen.WebSocketFrame;
-
-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);
- this.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/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
index 5faf72a..1f4623e 100644
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
+++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
@@ -1,13 +1,55 @@
package fi.iki.elonen.samples.echo;
-import fi.iki.elonen.NanoWebSocketServer;
-import fi.iki.elonen.WebSocket;
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.NanoWSD;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
/**
-* @author Paul S. Hawke (paul.hawke@gmail.com)
-* On: 4/23/14 at 10:31 PM
-*/
-class DebugWebSocketServer extends NanoWebSocketServer {
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 4/23/14 at 10:31 PM
+ */
+public class DebugWebSocketServer extends NanoWSD {
+
+ /**
+ * logger to log to.
+ */
+ private static final Logger LOG = Logger.getLogger(DebugWebSocketServer.class.getName());
+
private final boolean debug;
public DebugWebSocketServer(int port, boolean debug) {
@@ -16,7 +58,65 @@ class DebugWebSocketServer extends NanoWebSocketServer {
}
@Override
- public WebSocket openWebSocket(IHTTPSession handshake) {
- return new DebugWebSocket(handshake, debug);
+ protected WebSocket openWebSocket(IHTTPSession handshake) {
+ return new DebugWebSocket(this, handshake);
+ }
+
+ private static class DebugWebSocket extends WebSocket {
+
+ private final DebugWebSocketServer server;
+
+ public DebugWebSocket(DebugWebSocketServer server, IHTTPSession handshakeRequest) {
+ super(handshakeRequest);
+ this.server = server;
+ }
+
+ @Override
+ protected void onOpen() {
+ }
+
+ @Override
+ protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
+ if (server.debug) {
+ System.out.println("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
+ + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
+ }
+ }
+
+ @Override
+ protected void onMessage(WebSocketFrame message) {
+ try {
+ message.setUnmasked();
+ sendFrame(message);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected void onPong(WebSocketFrame pong) {
+ if (server.debug) {
+ System.out.println("P " + pong);
+ }
+ }
+
+ @Override
+ protected void onException(IOException exception) {
+ DebugWebSocketServer.LOG.log(Level.SEVERE, "exception occured", exception);
+ }
+
+ @Override
+ protected void debugFrameReceived(WebSocketFrame frame) {
+ if (server.debug) {
+ System.out.println("R " + frame);
+ }
+ }
+
+ @Override
+ protected void debugFrameSent(WebSocketFrame frame) {
+ if (server.debug) {
+ System.out.println("S " + frame);
+ }
+ }
}
}
diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
index b28a42c..e7d7e3b 100644
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
+++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
@@ -1,13 +1,47 @@
package fi.iki.elonen.samples.echo;
-import fi.iki.elonen.NanoWebSocketServer;
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
import java.io.IOException;
+import fi.iki.elonen.NanoWSD;
+
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);
+ NanoWSD ws = new DebugWebSocketServer(args.length > 0 ? Integer.parseInt(args[0]) : 9090, debugMode);
ws.start();
System.out.println("Server started, hit Enter to stop.\n");
try {
@@ -19,4 +53,3 @@ public class EchoSocketSample {
}
}
-
diff --git a/websocket/src/site/site.xml b/websocket/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/websocket/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+ <skin>
+ <groupId>org.apache.maven.skins</groupId>
+ <artifactId>maven-fluido-skin</artifactId>
+ <version>1.3.0</version>
+ </skin>
+ <bannerLeft>
+ <src>../images/nanohttpd_logo.png</src>
+ </bannerLeft>
+ <bannerRight>
+ <src>../images/nanohttpd_logo_text.png</src>
+ </bannerRight>
+ <publishDate position="left" format="yyyy-MM-dd" />
+ <version position="right" />
+ <poweredBy>
+ <logo name="Maven" href="http://maven.apache.org/"
+ img="http://maven.apache.org/images/logos/maven-feather.png" />
+ </poweredBy>
+ <custom>
+ <fluidoSkin>
+ <topBarEnabled>false</topBarEnabled>
+ <sideBarEnabled>true</sideBarEnabled>
+ <gitHub>
+ <projectId>Nanohttpd/nanohttpd</projectId>
+ <ribbonOrientation>right</ribbonOrientation>
+ <ribbonColor>black</ribbonColor>
+ </gitHub>
+ </fluidoSkin>
+ </custom>
+ <body>
+ <breadcrumbs>
+ <item name="${project.name}" href="index.html" />
+ </breadcrumbs>
+ <menu name="Documentation">
+ <item name="About" href="index.html" />
+ </menu>
+ <menu ref="modules" />
+ <menu ref="reports" />
+ </body>
+</project> \ No newline at end of file
diff --git a/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java b/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java
deleted file mode 100644
index 7cd06b7..0000000
--- a/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package fi.iki.elonen;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class NanoWebSocketServerTest {
- @Mock
- private NanoHTTPD.IHTTPSession session;
-
- private NanoWebSocketServer server;
-
- @Before
- public void setUp() {
- server = new NanoWebSocketServer(9090);
- }
-
- @Test(expected = Error.class)
- public void testMissingResponseFactoryThrowsErrorOnServe() {
- server.openWebSocket(session);
- }
-
- @Test
- public void testMissingResponseFactoryThrowsErrorWithCorrectMessageOnServe() {
- NanoWebSocketServer server = new NanoWebSocketServer(9090);
- try {
- server.openWebSocket(session);
- } catch (Error e) {
- assertEquals(NanoWebSocketServer.MISSING_FACTORY_MESSAGE, e.getMessage());
- }
- }
-} \ No newline at end of file
diff --git a/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java b/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
index 04a497b..8c083ee 100644
--- a/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
+++ b/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
@@ -1,120 +1,169 @@
package fi.iki.elonen;
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
-
-import fi.iki.elonen.NanoHTTPD.IHTTPSession;
-import fi.iki.elonen.NanoHTTPD.Response;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
-import static junit.framework.Assert.*;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import fi.iki.elonen.NanoHTTPD.IHTTPSession;
+import fi.iki.elonen.NanoHTTPD.Response;
+import fi.iki.elonen.NanoWSD.WebSocketFrame;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
@RunWith(MockitoJUnitRunner.class)
public class WebSocketResponseHandlerTest {
@Mock
private IHTTPSession session;
- @Mock
- private WebSocket webSocket;
- @Mock
- private IWebSocketFactory webSocketFactory;
- @Mock
- private Response response;
- @Captor
- private ArgumentCaptor<String> headerNameCaptor;
- @Captor
- private ArgumentCaptor<String> headerCaptor;
+
+ private NanoWSD nanoWebSocketServer;
private Map<String, String> headers;
- private WebSocketResponseHandler responseHandler;
+ private static class MockedWSD extends NanoWSD {
+
+ public MockedWSD(int port) {
+ super(port);
+ }
+
+ public MockedWSD(String hostname, int port) {
+ super(hostname, port);
+ }
+
+ @Override
+ protected WebSocket openWebSocket(IHTTPSession handshake) {
+ return new WebSocket(handshake) { // Dummy websocket inner class.
+
+ @Override
+ protected void onPong(WebSocketFrame pong) {
+ }
+
+ @Override
+ protected void onOpen() {
+ }
+
+ @Override
+ protected void onMessage(WebSocketFrame message) {
+ }
+
+ @Override
+ protected void onException(IOException exception) {
+ }
+
+ @Override
+ protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
+ }
+ };
+ }
+ }
@Before
public void setUp() {
- headers = new HashMap<String, String>();
- headers.put("upgrade", "websocket");
- headers.put("connection", "Upgrade");
- headers.put("sec-websocket-key", "x3JJHMbDL1EzLkh9GBhXDw==");
- headers.put("sec-websocket-protocol", "chat, superchat");
- headers.put("sec-websocket-version", "13");
-
- when(session.getHeaders()).thenReturn(headers);
- when(webSocketFactory.openWebSocket(any(IHTTPSession.class))).thenReturn(webSocket);
- when(webSocket.getHandshakeResponse()).thenReturn(response);
-
- responseHandler = new WebSocketResponseHandler(webSocketFactory);
+ this.nanoWebSocketServer = Mockito.mock(MockedWSD.class, Mockito.CALLS_REAL_METHODS);
+
+ this.headers = new HashMap<String, String>();
+ this.headers.put("upgrade", "websocket");
+ this.headers.put("connection", "Upgrade");
+ this.headers.put("sec-websocket-key", "x3JJHMbDL1EzLkh9GBhXDw==");
+ this.headers.put("sec-websocket-protocol", "chat, superchat");
+ this.headers.put("sec-websocket-version", "13");
+
+ when(this.session.getHeaders()).thenReturn(this.headers);
}
@Test
- public void testHandshakeReturnsResponseWithExpectedHeaders() {
- Response handshakeResponse = responseHandler.serve(session);
+ public void testConnectionHeaderHandlesKeepAlive_FixingFirefoxConnectIssue() {
+ this.headers.put("connection", "keep-alive, Upgrade");
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
- verify(webSocket).getHandshakeResponse();
assertNotNull(handshakeResponse);
- assertSame(response, handshakeResponse);
-
- verify(response, atLeast(1)).addHeader(headerNameCaptor.capture(), headerCaptor.capture());
- assertHeader(0, "sec-websocket-accept", "HSmrc0sMlYUkAGmm5OPpG2HaGWk=");
- assertHeader(1, "sec-websocket-protocol", "chat");
}
@Test
- public void testWrongWebsocketVersionReturnsErrorResponse() {
- headers.put("sec-websocket-version", "12");
-
- Response handshakeResponse = responseHandler.serve(session);
+ public void testHandshakeReturnsResponseWithExpectedHeaders() {
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
assertNotNull(handshakeResponse);
- assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
+
+ assertEquals(handshakeResponse.getHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT), "HSmrc0sMlYUkAGmm5OPpG2HaGWk=");
+ assertEquals(handshakeResponse.getHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL), "chat");
}
@Test
public void testMissingKeyReturnsErrorResponse() {
- headers.remove("sec-websocket-key");
+ this.headers.remove("sec-websocket-key");
- Response handshakeResponse = responseHandler.serve(session);
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
assertNotNull(handshakeResponse);
assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
}
@Test
- public void testWrongUpgradeHeaderReturnsNullResponse() {
- headers.put("upgrade", "not a websocket");
- Response handshakeResponse = responseHandler.serve(session);
- assertNull(handshakeResponse);
+ public void testWrongConnectionHeaderReturnsNullResponse() {
+ this.headers.put("connection", "Junk");
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+ assertNull(handshakeResponse.getHeader(NanoWSD.HEADER_UPGRADE));
}
@Test
- public void testWrongConnectionHeaderReturnsNullResponse() {
- headers.put("connection", "Junk");
- Response handshakeResponse = responseHandler.serve(session);
- assertNull(handshakeResponse);
+ public void testWrongUpgradeHeaderReturnsNullResponse() {
+ this.headers.put("upgrade", "not a websocket");
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+ assertNull(handshakeResponse.getHeader(NanoWSD.HEADER_UPGRADE));
}
@Test
- public void testConnectionHeaderHandlesKeepAlive_FixingFirefoxConnectIssue() {
- headers.put("connection", "keep-alive, Upgrade");
- Response handshakeResponse = responseHandler.serve(session);
+ public void testWrongWebsocketVersionReturnsErrorResponse() {
+ this.headers.put("sec-websocket-version", "12");
- verify(webSocket).getHandshakeResponse();
- assertNotNull(handshakeResponse);
- assertSame(response, handshakeResponse);
- }
+ Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
- private void assertHeader(int index, String name, String value) {
- assertEquals(name, headerNameCaptor.getAllValues().get(index));
- assertEquals(value, headerCaptor.getAllValues().get(index));
+ assertNotNull(handshakeResponse);
+ assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
}
}
diff --git a/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java
new file mode 100644
index 0000000..d8b96ab
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java
@@ -0,0 +1,102 @@
+package fi.iki.elonen.samples.echo;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoWSD;
+
+public class EchoWebSocketsTest {
+
+ private static NanoWSD server;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ EchoWebSocketsTest.server = new DebugWebSocketServer(9191, true);
+ EchoWebSocketsTest.server.start();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ EchoWebSocketsTest.server.stop();
+ }
+
+ @Test
+ public void testWebsocketClient() throws Exception {
+ String destUri = "ws://localhost:9191";
+
+ WebSocketClient client = new WebSocketClient();
+ SimpleEchoSocket socket = new SimpleEchoSocket();
+ socket.getToSendMessages().add("Hello");
+ socket.getToSendMessages().add("Thanks for the conversation.");
+ socket.getToSendMessages().add(createString(31000));
+ socket.getToSendMessages().add(createString(65400));
+ try {
+ client.start();
+ URI echoUri = new URI(destUri);
+ ClientUpgradeRequest request = new ClientUpgradeRequest();
+ client.connect(socket, echoUri, request);
+ System.out.printf("Connecting to : %s%n", echoUri);
+ socket.awaitClose(5, TimeUnit.SECONDS);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ } finally {
+ try {
+ client.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ Assert.assertEquals(4, socket.getReceivedMessages().size());
+ Assert.assertEquals("Hello", socket.getReceivedMessages().get(0));
+ Assert.assertEquals("Thanks for the conversation.", socket.getReceivedMessages().get(1));
+
+ }
+
+ private String createString(int i) {
+ StringBuilder builder = new StringBuilder();
+ while (builder.length() < i) {
+ builder.append("A very long text.");
+ }
+ return builder.toString();
+ }
+}
diff --git a/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java b/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java
new file mode 100644
index 0000000..45524d2
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java
@@ -0,0 +1,104 @@
+package fi.iki.elonen.samples.echo;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+/**
+ * Basic Echo Client Socket
+ */
+@WebSocket(maxTextMessageSize = 64 * 1024)
+public class SimpleEchoSocket {
+
+ private final List<String> receivedMessages = new ArrayList<String>();
+
+ private final List<String> toSendMessages = new ArrayList<String>();
+
+ private final CountDownLatch closeLatch;
+
+ public SimpleEchoSocket() {
+ this.closeLatch = new CountDownLatch(1);
+ }
+
+ public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException {
+ return this.closeLatch.await(duration, unit);
+ }
+
+ public List<String> getReceivedMessages() {
+ return this.receivedMessages;
+ }
+
+ public List<String> getToSendMessages() {
+ return this.toSendMessages;
+ }
+
+ @OnWebSocketClose
+ public void onClose(int statusCode, String reason) {
+ System.out.printf("Connection closed: %d - %s%n", statusCode, reason);
+ this.closeLatch.countDown();
+ }
+
+ @OnWebSocketConnect
+ public void onConnect(Session session) {
+ System.out.printf("Got connect: %s%n", session);
+ try {
+ Future<Void> fut;
+
+ for (String message : this.toSendMessages) {
+ fut = session.getRemote().sendStringByFuture(message);
+ fut.get(5, TimeUnit.SECONDS);
+ }
+ session.close(StatusCode.NORMAL, "I'm done");
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(String msg) {
+ System.out.printf("Got msg: %s%n", msg);
+ this.receivedMessages.add(msg);
+ }
+}
diff --git a/websocket/src/test/resources/echo-test.html b/websocket/src/test/resources/echo-test.html
index 4b60b80..3f036f2 100644
--- a/websocket/src/test/resources/echo-test.html
+++ b/websocket/src/test/resources/echo-test.html
@@ -1,3 +1,35 @@
+<!--
+ #%L
+ NanoHttpd-Websocket
+ %%
+ Copyright (C) 2012 - 2015 nanohttpd
+ %%
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ 3. Neither the name of the nanohttpd nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ OF THE POSSIBILITY OF SUCH DAMAGE.
+ #L%
+ -->
<html>
<head>
<meta charset="utf-8"/>