diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..1cad1b9
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion '26.0.2'
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:25.0.1'
+ testCompile 'junit:junit:4.12'
+}
diff --git a/proguard-rules.pro b/proguard-rules.pro
new file mode 100644
index 0000000..3a21f04
--- /dev/null
+++ b/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Users\android\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/src/androidTest/java/com/neovisionaries/ws/client/ExampleInstrumentedTest.java b/src/androidTest/java/com/neovisionaries/ws/client/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..34948de
--- /dev/null
+++ b/src/androidTest/java/com/neovisionaries/ws/client/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.neovisionaries.ws.client;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.neovisionaries.ws.client.test", appContext.getPackageName());
+ }
+}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ecf8b1f
--- /dev/null
+++ b/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/neovisionaries/ws/client/Address.java b/src/main/java/com/neovisionaries/ws/client/Address.java
new file mode 100644
index 0000000..af5487c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Address.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.net.InetSocketAddress;
+
+
+class Address {
+ private final String mHost;
+ private final int mPort;
+ private transient String mString;
+
+
+ Address(String host, int port) {
+ mHost = host;
+ mPort = port;
+ }
+
+
+ InetSocketAddress toInetSocketAddress() {
+ return new InetSocketAddress(mHost, mPort);
+ }
+
+
+ String getHostname() {
+ return mHost;
+ }
+
+
+ @Override
+ public String toString() {
+ if (mString == null) {
+ mString = String.format("%s:%d", mHost, mPort);
+ }
+
+ return mString;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/Base64.java b/src/main/java/com/neovisionaries/ws/client/Base64.java
new file mode 100644
index 0000000..e1ff788
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Base64.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class Base64 {
+ private static final byte[] INDEX_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+ };
+
+
+ public static String encode(String data) {
+ if (data == null) {
+ return null;
+ }
+
+ return encode(Misc.getBytesUTF8(data));
+ }
+
+
+ public static String encode(byte[] data) {
+ if (data == null) {
+ return null;
+ }
+
+ int capacity = (((((data.length * 8) + 5) / 6) + 3) / 4) * 4;
+
+ StringBuilder builder = new StringBuilder(capacity);
+
+ for (int bitIndex = 0; ; bitIndex += 6) {
+ int bits = extractBits(data, bitIndex);
+
+ if (bits < 0) {
+ break;
+ }
+
+ builder.append((char) INDEX_TABLE[bits]);
+ }
+
+ for (int i = builder.length(); i < capacity; ++i) {
+ builder.append('=');
+ }
+
+ return builder.toString();
+ }
+
+
+ private static int extractBits(byte[] data, int bitIndex) {
+ int byteIndex = bitIndex / 8;
+ byte nextByte;
+
+ if (data.length <= byteIndex) {
+ return -1;
+ } else if (data.length - 1 == byteIndex) {
+ nextByte = 0;
+ } else {
+ nextByte = data[byteIndex + 1];
+ }
+
+ switch ((bitIndex % 24) / 6) {
+ case 0:
+ return ((data[byteIndex] >> 2) & 0x3F);
+
+ case 1:
+ return (((data[byteIndex] << 4) & 0x30) | ((nextByte >> 4) & 0x0F));
+
+ case 2:
+ return (((data[byteIndex] << 2) & 0x3C) | ((nextByte >> 6) & 0x03));
+
+ case 3:
+ return (data[byteIndex] & 0x3F);
+
+ default:
+ // Never reach here.
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ByteArray.java b/src/main/java/com/neovisionaries/ws/client/ByteArray.java
new file mode 100644
index 0000000..a486d72
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ByteArray.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * Expandable byte array with byte-basis and bit-basis operations.
+ */
+class ByteArray {
+ private static final int ADDITIONAL_BUFFER_SIZE = 1024;
+
+ // The buffer.
+ private ByteBuffer mBuffer;
+
+ // The current length.
+ private int mLength;
+
+
+ /**
+ * Constructor with initial capacity.
+ *
+ * @param capacity
+ * Initial capacity for the internal buffer.
+ */
+ public ByteArray(int capacity) {
+ mBuffer = ByteBuffer.allocate(capacity);
+ mLength = 0;
+ }
+
+
+ /**
+ * Constructor with initial data. The length of the data is used
+ * as the initial capacity of the internal buffer.
+ *
+ * @param data
+ * Initial data.
+ */
+ public ByteArray(byte[] data) {
+ mBuffer = ByteBuffer.wrap(data);
+ mLength = data.length;
+ }
+
+
+ /**
+ * The length of the data.
+ */
+ public int length() {
+ return mLength;
+ }
+
+
+ /**
+ * Get a byte at the index.
+ */
+ public byte get(int index) throws IndexOutOfBoundsException {
+ if (index < 0 || mLength <= index) {
+ // Bad index.
+ throw new IndexOutOfBoundsException(
+ String.format("Bad index: index=%d, length=%d", index, mLength));
+ }
+
+ return mBuffer.get(index);
+ }
+
+
+ /**
+ * Expand the size of the internal buffer.
+ */
+ private void expandBuffer(int newBufferSize) {
+ // Allocate a new buffer.
+ ByteBuffer newBuffer = ByteBuffer.allocate(newBufferSize);
+
+ // Copy the content of the current buffer to the new buffer.
+ int oldPosition = mBuffer.position();
+ mBuffer.position(0);
+ newBuffer.put(mBuffer);
+ newBuffer.position(oldPosition);
+
+ // Replace the buffers.
+ mBuffer = newBuffer;
+ }
+
+
+ /**
+ * Add a byte at the current position.
+ */
+ public void put(int data) {
+ // If the buffer is small.
+ if (mBuffer.capacity() < (mLength + 1)) {
+ expandBuffer(mLength + ADDITIONAL_BUFFER_SIZE);
+ }
+
+ mBuffer.put((byte) data);
+ ++mLength;
+ }
+
+
+ /**
+ * Add data at the current position.
+ *
+ * @param source
+ * Source data.
+ */
+ public void put(byte[] source) {
+ // If the buffer is small.
+ if (mBuffer.capacity() < (mLength + source.length)) {
+ expandBuffer(mLength + source.length + ADDITIONAL_BUFFER_SIZE);
+ }
+
+ mBuffer.put(source);
+ mLength += source.length;
+ }
+
+
+ /**
+ * Add data at the current position.
+ *
+ * @param source
+ * Source data.
+ *
+ * @param index
+ * The index in the source data. Data from the index is copied.
+ *
+ * @param length
+ * The length of data to copy.
+ */
+ public void put(byte[] source, int index, int length) {
+ // If the buffer is small.
+ if (mBuffer.capacity() < (mLength + length)) {
+ expandBuffer(mLength + length + ADDITIONAL_BUFFER_SIZE);
+ }
+
+ mBuffer.put(source, index, length);
+ mLength += length;
+ }
+
+
+ /**
+ * Add data at the current position.
+ *
+ * @param source
+ * Source data.
+ *
+ * @param index
+ * The index in the source data. Data from the index is copied.
+ *
+ * @param length
+ * The length of data to copy.
+ */
+ public void put(ByteArray source, int index, int length) {
+ put(source.mBuffer.array(), index, length);
+ }
+
+
+ /**
+ * Convert to a byte array (byte[]).
+ */
+ public byte[] toBytes() {
+ return toBytes(0);
+ }
+
+
+ public byte[] toBytes(int beginIndex) {
+ return toBytes(beginIndex, length());
+ }
+
+
+ public byte[] toBytes(int beginIndex, int endIndex) {
+ int len = endIndex - beginIndex;
+
+ if (len < 0 || beginIndex < 0 || mLength < endIndex) {
+ throw new IllegalArgumentException(
+ String.format("Bad range: beginIndex=%d, endIndex=%d, length=%d",
+ beginIndex, endIndex, mLength));
+ }
+
+ byte[] bytes = new byte[len];
+
+ if (len != 0) {
+ System.arraycopy(mBuffer.array(), beginIndex, bytes, 0, len);
+ }
+
+ return bytes;
+ }
+
+
+ public void clear() {
+ mBuffer.clear();
+ mBuffer.position(0);
+ mLength = 0;
+ }
+
+
+ public void shrink(int size) {
+ if (mBuffer.capacity() <= size) {
+ return;
+ }
+
+ int endIndex = mLength;
+ int beginIndex = mLength - size;
+
+ byte[] bytes = toBytes(beginIndex, endIndex);
+
+ mBuffer = ByteBuffer.wrap(bytes);
+ mBuffer.position(bytes.length);
+ mLength = bytes.length;
+ }
+
+
+ public boolean getBit(int bitIndex) {
+ int index = bitIndex / 8;
+ int shift = bitIndex % 8;
+ int value = get(index);
+
+ // Return true if the bit pointed to by bitIndex is set.
+ return ((value & (1 << shift)) != 0);
+ }
+
+
+ public int getBits(int bitIndex, int nBits) {
+ int number = 0;
+ int weight = 1;
+
+ // Convert consecutive bits into a number.
+ for (int i = 0; i < nBits; ++i, weight *= 2) {
+ // getBit() returns true if the bit is set.
+ if (getBit(bitIndex + i)) {
+ number += weight;
+ }
+ }
+
+ return number;
+ }
+
+
+ public int getHuffmanBits(int bitIndex, int nBits) {
+ int number = 0;
+ int weight = 1;
+
+ // Convert consecutive bits into a number.
+ //
+ // Note that 'i' is initialized by 'nBits - 1', not by 1.
+ // This is because "3.1.1. Packing into bytes" in RFC 1951
+ // says as follows:
+ //
+ // Huffman codes are packed starting with the most
+ // significant bit of the code.
+ //
+ for (int i = nBits - 1; 0 <= i; --i, weight *= 2) {
+ // getBit() returns true if the bit is set.
+ if (getBit(bitIndex + i)) {
+ number += weight;
+ }
+ }
+
+ return number;
+ }
+
+
+ public boolean readBit(int[] bitIndex) {
+ boolean result = getBit(bitIndex[0]);
+
+ ++bitIndex[0];
+
+ return result;
+ }
+
+
+ public int readBits(int[] bitIndex, int nBits) {
+ int number = getBits(bitIndex[0], nBits);
+
+ bitIndex[0] += nBits;
+
+ return number;
+ }
+
+
+ public void setBit(int bitIndex, boolean bit) {
+ int index = bitIndex / 8;
+ int shift = bitIndex % 8;
+ int value = get(index);
+
+ if (bit) {
+ value |= (1 << shift);
+ } else {
+ value &= ~(1 << shift);
+ }
+
+ mBuffer.put(index, (byte) value);
+ }
+
+
+ public void clearBit(int bitIndex) {
+ setBit(bitIndex, false);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ConnectThread.java b/src/main/java/com/neovisionaries/ws/client/ConnectThread.java
new file mode 100644
index 0000000..6e2352c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ConnectThread.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class ConnectThread extends WebSocketThread {
+ public ConnectThread(WebSocket ws) {
+ super("ConnectThread", ws, ThreadType.CONNECT_THREAD);
+ }
+
+
+ @Override
+ public void runMain() {
+ try {
+ mWebSocket.connect();
+ } catch (WebSocketException e) {
+ handleError(e);
+ }
+ }
+
+
+ private void handleError(WebSocketException cause) {
+ ListenerManager manager = mWebSocket.getListenerManager();
+
+ manager.callOnError(cause);
+ manager.callOnConnectError(cause);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/Connectable.java b/src/main/java/com/neovisionaries/ws/client/Connectable.java
new file mode 100644
index 0000000..d7c2cde
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Connectable.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.concurrent.Callable;
+
+
+/**
+ * An implementation of {@link Callable} interface that calls
+ * {@link WebSocket#connect()}.
+ *
+ * @since 1.7
+ */
+class Connectable implements Callable {
+ private final WebSocket mWebSocket;
+
+
+ public Connectable(WebSocket ws) {
+ mWebSocket = ws;
+ }
+
+
+ @Override
+ public WebSocket call() throws WebSocketException {
+ return mWebSocket.connect();
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/CounterPayloadGenerator.java b/src/main/java/com/neovisionaries/ws/client/CounterPayloadGenerator.java
new file mode 100644
index 0000000..e0e67d9
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/CounterPayloadGenerator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class CounterPayloadGenerator implements PayloadGenerator {
+ private long mCount;
+
+
+ @Override
+ public byte[] generate() {
+ return Misc.getBytesUTF8(String.valueOf(increment()));
+ }
+
+
+ private long increment() {
+ // Increment the counter.
+ mCount = Math.max(mCount + 1, 1);
+
+ return mCount;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/DeflateCompressor.java b/src/main/java/com/neovisionaries/ws/client/DeflateCompressor.java
new file mode 100644
index 0000000..11a9ead
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/DeflateCompressor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+
+/**
+ * DEFLATE (RFC 1951)
+ * compressor implementation.
+ */
+class DeflateCompressor {
+ public static byte[] compress(byte[] input) throws IOException {
+ // Destination where compressed data will be stored.
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ // Create a compressor.
+ Deflater deflater = createDeflater();
+ DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater);
+
+ // Compress the data.
+ //
+ // Some other implementations such as Jetty and Tyrus use
+ // Deflater.deflate(byte[], int, int, int) with Deflate.SYNC_FLUSH,
+ // but this implementation does not do it intentionally because the
+ // method and the constant value are not available before Java 7.
+ dos.write(input, 0, input.length);
+ dos.close();
+
+ // Release the resources held by the compressor.
+ deflater.end();
+
+ // Retrieve the compressed data.
+ return baos.toByteArray();
+ }
+
+
+ private static Deflater createDeflater() {
+ // The second argument (nowrap) is true to get only DEFLATE
+ // blocks without the ZLIB header and checksum fields.
+ return new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/DeflateDecompressor.java b/src/main/java/com/neovisionaries/ws/client/DeflateDecompressor.java
new file mode 100644
index 0000000..2c230c0
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/DeflateDecompressor.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * DEFLATE (RFC 1951)
+ * decompressor implementation from scratch.
+ */
+class DeflateDecompressor {
+ public static void decompress(ByteArray input, ByteArray output) throws FormatException {
+ decompress(input, 0, output);
+ }
+
+
+ private static void decompress(ByteArray input, int index, ByteArray output) throws FormatException {
+ // The data is compressed on a bit basis, so use a bit index.
+ int[] bitIndex = new int[1];
+ bitIndex[0] = index * 8;
+
+ // Process all blocks one by one until the end.
+ // inflateBlock() returns false if no more block exists.
+ while (inflateBlock(input, bitIndex, output)) {
+ }
+ }
+
+
+ private static boolean inflateBlock(ByteArray input, int[] bitIndex, ByteArray output) throws FormatException {
+ // Each block has a block header which consists of 3 bits.
+ // See 3.2.3. of RFC 1951.
+
+ // The first bit indicates whether the block is the last one or not.
+ boolean last = input.readBit(bitIndex);
+
+ // The combination of the second and the third bits indicate the
+ // compression type of the block. Compression types are as follows:
+ //
+ // 00: No compression.
+ // 01: Compressed with fixed Huffman codes
+ // 10: Compressed with dynamic Huffman codes
+ // 11: Reserved (error)
+ //
+ int type = input.readBits(bitIndex, 2);
+
+ switch (type) {
+ // No compression
+ case 0:
+ inflatePlainBlock(input, bitIndex, output);
+ break;
+
+ // Compressed with fixed Huffman codes
+ case 1:
+ inflateFixedBlock(input, bitIndex, output);
+ break;
+
+ // Compressed with dynamic Huffman codes
+ case 2:
+ inflateDynamicBlock(input, bitIndex, output);
+ break;
+
+ // Bad format
+ default:
+ // Bad compression type at the bit index.
+ String message = String.format(
+ "[%s] Bad compression type '11' at the bit index '%d'.",
+ DeflateDecompressor.class.getSimpleName(), bitIndex[0]);
+
+ throw new FormatException(message);
+ }
+
+ // If no more data are available.
+ if (input.length() <= (bitIndex[0] / 8)) {
+ // Last even if BFINAL bit is false.
+ last = true;
+ }
+
+ // Return true if this block is not the last one.
+ return !last;
+ }
+
+
+ private static void inflatePlainBlock(ByteArray input, int[] bitIndex, ByteArray output) {
+ // 3.2.4 Non-compressed blocks (BTYPE=00)
+
+ // Skip any remaining bits in current partially processed byte.
+ int bi = (bitIndex[0] + 7) & ~7;
+
+ // Data copy is performed on a byte basis, so convert the bit index
+ // to a byte index.
+ int index = bi / 8;
+
+ // LEN: 2 bytes. The data length.
+ int len = (input.get(index) & 0xFF) + (input.get(index + 1) & 0xFF) * 256;
+
+ // NLEN: 2 bytes. The one's complement of LEN.
+
+ // Skip LEN and NLEN.
+ index += 4;
+
+ // Copy the data to the output.
+ output.put(input, index, len);
+
+ // Make the bitIndex point to the bit next to
+ // the end of the copied data.
+ bitIndex[0] = (index + len) * 8;
+ }
+
+
+ private static void inflateFixedBlock(ByteArray input, int[] bitIndex, ByteArray output) throws FormatException {
+ // 3.2.6 Compression with fixed Huffman codes (BTYPE=01)
+
+ // Inflate the compressed data using the pre-defined
+ // conversion tables. The specification says in 3.2.2
+ // as follows.
+ //
+ // The only differences between the two compressed
+ // cases is how the Huffman codes for the literal/
+ // length and distance alphabets are defined.
+ //
+ // The "two compressed cases" in the above sentence are
+ // "fixed Huffman codes" and "dynamic Huffman codes".
+ inflateData(input, bitIndex, output,
+ FixedLiteralLengthHuffman.getInstance(),
+ FixedDistanceHuffman.getInstance());
+ }
+
+
+ private static void inflateDynamicBlock(ByteArray input, int[] bitIndex, ByteArray output) throws FormatException {
+ // 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
+
+ // Read 2 tables. One is a table to convert "code value of literal/length
+ // alphabet" into "literal/length symbol". The other is a table to convert
+ // "code value of distance alphabet" into "distance symbol".
+ Huffman[] tables = new Huffman[2];
+ DeflateUtil.readDynamicTables(input, bitIndex, tables);
+
+ // The actual compressed data of this block. The data are encoded using
+ // the literal/length and distance Huffman codes that were parsed above.
+ inflateData(input, bitIndex, output, tables[0], tables[1]);
+ }
+
+
+ private static void inflateData(ByteArray input, int[] bitIndex, ByteArray output, Huffman literalLengthHuffman, Huffman distanceHuffman) throws FormatException {
+ // 3.2.5 Compressed blocks (length and distance codes)
+
+ while (true) {
+ // Read a literal/length symbol from the input.
+ int literalLength = literalLengthHuffman.readSym(input, bitIndex);
+
+ // Symbol value '256' indicates the end.
+ if (literalLength == 256) {
+ // End of this data.
+ break;
+ }
+
+ // Symbol values from 0 to 255 represent literal values.
+ if (0 <= literalLength && literalLength <= 255) {
+ // Output as is.
+ output.put(literalLength);
+ continue;
+ }
+
+ // Symbol values from 257 to 285 represent pairs.
+ // Depending on symbol values, some extra bits in the input may be
+ // consumed to compute the length.
+ int length = DeflateUtil.readLength(input, bitIndex, literalLength);
+
+ // Read the distance from the input.
+ int distance = DeflateUtil.readDistance(input, bitIndex, distanceHuffman);
+
+ // Extract some data from the output buffer and copy them.
+ duplicate(length, distance, output);
+ }
+ }
+
+
+ private static void duplicate(int length, int distance, ByteArray output) {
+ // Get the number of bytes written so far.
+ int sourceLength = output.length();
+
+ // An array to finally append to the output.
+ byte[] target = new byte[length];
+
+ // The position from which to start copying data.
+ int initialPosition = sourceLength - distance;
+ int sourceIndex = initialPosition;
+
+ for (int targetIndex = 0; targetIndex < length; ++targetIndex, ++sourceIndex) {
+ if (sourceLength <= sourceIndex) {
+ // Reached the end of the current output buffer.
+ // The specification says as follows in 3.2.3.
+ //
+ // Note also that the referenced string may
+ // overlap the current position; for example,
+ // if the last 2 bytes decoded have values X
+ // and Y, a string reference with adds X,Y,X,Y,X to the output
+ // stream.
+
+ // repeat.
+ sourceIndex = initialPosition;
+ }
+
+ target[targetIndex] = output.get(sourceIndex);
+ }
+
+ // Append the duplicated bytes to the output.
+ output.put(target);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/DeflateUtil.java b/src/main/java/com/neovisionaries/ws/client/DeflateUtil.java
new file mode 100644
index 0000000..9872c18
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/DeflateUtil.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Utility methods for DEFLATE (RFC 1951).
+ */
+class DeflateUtil {
+ private static int[] INDICES_FROM_CODE_LENGTH_ORDER = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
+
+
+ public static void readDynamicTables(ByteArray input, int[] bitIndex, Huffman[] tables) throws FormatException {
+ // 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
+
+ // 5 Bits: HLIT, The number of Literal/Length codes - 257 (257 - 286)
+ int hlit = input.readBits(bitIndex, 5) + 257;
+
+ // 5 Bits: HDIST, The number of Distance codes - 1 (1 - 32)
+ int hdist = input.readBits(bitIndex, 5) + 1;
+
+ // 4 Bits: HCLEN, The number of Code Length codes - 4 (4 - 19)
+ int hclen = input.readBits(bitIndex, 4) + 4;
+
+ // (hclen * 3) bits: code lengths of "values of code length".
+ //
+ // Note that "values of code lengths" (which ranges from 0 to 18)
+ // themselves are compressed using Huffman code. In addition,
+ // the order here is strange.
+ int[] codeLengthsFromCodeLengthValue = new int[19];
+ for (int i = 0; i < hclen; ++i) {
+ byte codeLengthOfCodeLengthValue = (byte) input.readBits(bitIndex, 3);
+
+ // The strange order is converted into a normal index here.
+ int index = codeLengthOrderToIndex(i);
+
+ codeLengthsFromCodeLengthValue[index] = codeLengthOfCodeLengthValue;
+ }
+
+ // Create a table to convert "code value of code length value" into
+ // "code length value".
+ Huffman codeLengthHuffman = new Huffman(codeLengthsFromCodeLengthValue);
+
+ // hlit code lengths for literal/length alphabet. The code lengths are
+ // encoded using the code length Huffman code that was parsed above.
+ int[] codeLengthsFromLiteralLengthCode = new int[hlit];
+ readCodeLengths(input, bitIndex, codeLengthsFromLiteralLengthCode, codeLengthHuffman);
+
+ // Create a table to convert "code value of literal/length alphabet"
+ // into "literal/length symbol".
+ Huffman literalLengthHuffman = new Huffman(codeLengthsFromLiteralLengthCode);
+
+ // hdist code lengths for the distance alphabet. The code lengths are
+ // encoded using the code length Huffman code that was parsed above.
+ int[] codeLengthsFromDistanceCode = new int[hdist];
+ readCodeLengths(input, bitIndex, codeLengthsFromDistanceCode, codeLengthHuffman);
+
+ // Create a table to convert "code value of distance alphabet" into
+ // "distance symbol".
+ Huffman distanceHuffman = new Huffman(codeLengthsFromDistanceCode);
+
+ tables[0] = literalLengthHuffman;
+ tables[1] = distanceHuffman;
+ }
+
+
+ private static void readCodeLengths(ByteArray input, int bitIndex[], int[] codeLengths, Huffman codeLengthHuffman) throws FormatException {
+ // 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
+
+ for (int i = 0; i < codeLengths.length; ++i) {
+ // Read a symbol value of code length.
+ int codeLength = codeLengthHuffman.readSym(input, bitIndex);
+
+ // Code lengths from 0 to 15 represent 0 to 15, respectively,
+ // meaning no more extra interpretation is needed.
+ if (0 <= codeLength && codeLength <= 15) {
+ // As is.
+ codeLengths[i] = codeLength;
+ continue;
+ }
+
+ int repeatCount;
+
+ switch (codeLength) {
+ case 16:
+ // Copy the previous code length for 3 - 6 times.
+ // The next 2 bits (+3) indicate repeat count.
+ codeLength = codeLengths[i - 1];
+ repeatCount = input.readBits(bitIndex, 2) + 3;
+ break;
+
+ case 17:
+ // Copy a code length of 0 for 3 - 10 times.
+ // The next 3 bits (+3) indicate repeat count.
+ codeLength = 0;
+ repeatCount = input.readBits(bitIndex, 3) + 3;
+ break;
+
+ case 18:
+ // Copy a code length of 0 for 11 - 138 times.
+ // The next 7 bits (+11) indicate repeat count.
+ codeLength = 0;
+ repeatCount = input.readBits(bitIndex, 7) + 11;
+ break;
+
+ default:
+ // Bad code length.
+ String message = String.format(
+ "[%s] Bad code length '%d' at the bit index '%d'.",
+ DeflateUtil.class.getSimpleName(), codeLength, bitIndex);
+
+ throw new FormatException(message);
+ }
+
+ // Copy the code length as many times as specified.
+ for (int j = 0; j < repeatCount; ++j) {
+ codeLengths[i + j] = codeLength;
+ }
+
+ // Skip the range filled by the above copy.
+ i += repeatCount - 1;
+ }
+ }
+
+
+ private static int codeLengthOrderToIndex(int order) {
+ // 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
+ //
+ // See the description about "(HCLEN + 4) x 3 bits" in the
+ // specification.
+ return INDICES_FROM_CODE_LENGTH_ORDER[order];
+ }
+
+
+ public static int readLength(ByteArray input, int[] bitIndex, int literalLength) throws FormatException {
+ // 3.2.5 Compressed blocks (length and distance code)
+
+ int baseValue;
+ int nBits;
+
+ switch (literalLength) {
+ case 257:
+ case 258:
+ case 259:
+ case 260:
+ case 261:
+ case 262:
+ case 263:
+ case 264:
+ return (literalLength - 254);
+
+ case 265:
+ baseValue = 11;
+ nBits = 1;
+ break;
+ case 266:
+ baseValue = 13;
+ nBits = 1;
+ break;
+ case 267:
+ baseValue = 15;
+ nBits = 1;
+ break;
+ case 268:
+ baseValue = 17;
+ nBits = 1;
+ break;
+ case 269:
+ baseValue = 19;
+ nBits = 2;
+ break;
+ case 270:
+ baseValue = 23;
+ nBits = 2;
+ break;
+ case 271:
+ baseValue = 27;
+ nBits = 2;
+ break;
+ case 272:
+ baseValue = 31;
+ nBits = 2;
+ break;
+ case 273:
+ baseValue = 35;
+ nBits = 3;
+ break;
+ case 274:
+ baseValue = 43;
+ nBits = 3;
+ break;
+ case 275:
+ baseValue = 51;
+ nBits = 3;
+ break;
+ case 276:
+ baseValue = 59;
+ nBits = 3;
+ break;
+ case 277:
+ baseValue = 67;
+ nBits = 4;
+ break;
+ case 278:
+ baseValue = 83;
+ nBits = 4;
+ break;
+ case 279:
+ baseValue = 99;
+ nBits = 4;
+ break;
+ case 280:
+ baseValue = 115;
+ nBits = 4;
+ break;
+ case 281:
+ baseValue = 131;
+ nBits = 5;
+ break;
+ case 282:
+ baseValue = 163;
+ nBits = 5;
+ break;
+ case 283:
+ baseValue = 195;
+ nBits = 5;
+ break;
+ case 284:
+ baseValue = 227;
+ nBits = 5;
+ break;
+ case 285:
+ return 258;
+ default:
+ // Bad literal/length code.
+ String message = String.format(
+ "[%s] Bad literal/length code '%d' at the bit index '%d'.",
+ DeflateUtil.class.getSimpleName(), literalLength, bitIndex[0]);
+
+ throw new FormatException(message);
+ }
+
+ // Read a value to add to the base value.
+ int n = input.readBits(bitIndex, nBits);
+
+ return baseValue + n;
+ }
+
+
+ public static int readDistance(ByteArray input, int[] bitIndex, Huffman distanceHuffman) throws FormatException {
+ // 3.2.5 Compressed blocks (length and distance code)
+
+ // Read a distance code from the input.
+ // It is expected to range from 0 to 29.
+ int code = distanceHuffman.readSym(input, bitIndex);
+
+ int baseValue;
+ int nBits;
+
+ switch (code) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ return code + 1;
+
+ case 4:
+ baseValue = 5;
+ nBits = 1;
+ break;
+ case 5:
+ baseValue = 7;
+ nBits = 1;
+ break;
+ case 6:
+ baseValue = 9;
+ nBits = 2;
+ break;
+ case 7:
+ baseValue = 13;
+ nBits = 2;
+ break;
+ case 8:
+ baseValue = 17;
+ nBits = 3;
+ break;
+ case 9:
+ baseValue = 25;
+ nBits = 3;
+ break;
+ case 10:
+ baseValue = 33;
+ nBits = 4;
+ break;
+ case 11:
+ baseValue = 49;
+ nBits = 4;
+ break;
+ case 12:
+ baseValue = 65;
+ nBits = 5;
+ break;
+ case 13:
+ baseValue = 97;
+ nBits = 5;
+ break;
+ case 14:
+ baseValue = 129;
+ nBits = 6;
+ break;
+ case 15:
+ baseValue = 193;
+ nBits = 6;
+ break;
+ case 16:
+ baseValue = 257;
+ nBits = 7;
+ break;
+ case 17:
+ baseValue = 385;
+ nBits = 7;
+ break;
+ case 18:
+ baseValue = 513;
+ nBits = 8;
+ break;
+ case 19:
+ baseValue = 769;
+ nBits = 8;
+ break;
+ case 20:
+ baseValue = 1025;
+ nBits = 9;
+ break;
+ case 21:
+ baseValue = 1537;
+ nBits = 9;
+ break;
+ case 22:
+ baseValue = 2049;
+ nBits = 10;
+ break;
+ case 23:
+ baseValue = 3073;
+ nBits = 10;
+ break;
+ case 24:
+ baseValue = 4097;
+ nBits = 11;
+ break;
+ case 25:
+ baseValue = 6145;
+ nBits = 11;
+ break;
+ case 26:
+ baseValue = 8193;
+ nBits = 12;
+ break;
+ case 27:
+ baseValue = 12289;
+ nBits = 12;
+ break;
+ case 28:
+ baseValue = 16385;
+ nBits = 13;
+ break;
+ case 29:
+ baseValue = 24577;
+ nBits = 13;
+ break;
+ default:
+ // Distance codes 30-31 will never actually occur
+ // in the compressed data, the specification says.
+
+ // Bad distance code.
+ String message = String.format(
+ "[%s] Bad distance code '%d' at the bit index '%d'.",
+ DeflateUtil.class.getSimpleName(), code, bitIndex[0]);
+
+ throw new FormatException(message);
+ }
+
+ // Read a value to add to the base value.
+ int n = input.readBits(bitIndex, nBits);
+
+ return baseValue + n;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/DistinguishedNameParser.java b/src/main/java/com/neovisionaries/ws/client/DistinguishedNameParser.java
new file mode 100644
index 0000000..f3bf0b0
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/DistinguishedNameParser.java
@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.neovisionaries.ws.client;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A distinguished name (DN) parser. This parser only supports extracting a
+ * string value from a DN. It doesn't support values in the hex-string style.
+ */
+final class DistinguishedNameParser {
+ private final String dn;
+ private final int length;
+ private int pos;
+ private int beg;
+ private int end;
+
+ /** Temporary variable to store positions of the currently parsed item. */
+ private int cur;
+
+ /** Distinguished name characters. */
+ private char[] chars;
+
+ public DistinguishedNameParser(X500Principal principal) {
+ // RFC2253 is used to ensure we get attributes in the reverse
+ // order of the underlying ASN.1 encoding, so that the most
+ // significant values of repeated attributes occur first.
+ this.dn = principal.getName(X500Principal.RFC2253);
+ this.length = this.dn.length();
+ }
+
+ // gets next attribute type: (ALPHA 1*keychar) / oid
+ private String nextAT() {
+ // skip preceding space chars, they can present after
+ // comma or semicolon (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+ if (pos == length) {
+ return null; // reached the end of DN
+ }
+
+ // mark the beginning of attribute type
+ beg = pos;
+
+ // attribute type chars
+ pos++;
+ for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
+ // we don't follow exact BNF syntax here:
+ // accept any char except space and '='
+ }
+ if (pos >= length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ // mark the end of attribute type
+ end = pos;
+
+ // skip trailing space chars between attribute type and '='
+ // (compatibility with RFC 1779)
+ if (chars[pos] == ' ') {
+ for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
+ }
+
+ if (chars[pos] != '=' || pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+ }
+
+ pos++; //skip '=' char
+
+ // skip space chars between '=' and attribute value
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+
+ // in case of oid attribute type skip its prefix: "oid." or "OID."
+ // (compatibility with RFC 1779)
+ if ((end - beg > 4) && (chars[beg + 3] == '.') && (chars[beg] == 'O' || chars[beg] == 'o') && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
+ beg += 4;
+ }
+
+ return new String(chars, beg, end - beg);
+ }
+
+ // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
+ private String quotedAV() {
+ pos++;
+ beg = pos;
+ end = beg;
+ while (true) {
+
+ if (pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ if (chars[pos] == '"') {
+ // enclosing quotation was found
+ pos++;
+ break;
+ } else if (chars[pos] == '\\') {
+ chars[end] = getEscaped();
+ } else {
+ // shift char: required for string with escaped chars
+ chars[end] = chars[pos];
+ }
+ pos++;
+ end++;
+ }
+
+ // skip trailing space chars before comma or semicolon.
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+
+ return new String(chars, beg, end - beg);
+ }
+
+ // gets hex string attribute value: "#" hexstring
+ private String hexAV() {
+ if (pos + 4 >= length) {
+ // encoded byte array must be not less then 4 c
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ beg = pos; // store '#' position
+ pos++;
+ while (true) {
+
+ // check for end of attribute value
+ // looks for space and component separators
+ if (pos == length || chars[pos] == '+' || chars[pos] == ',' || chars[pos] == ';') {
+ end = pos;
+ break;
+ }
+
+ if (chars[pos] == ' ') {
+ end = pos;
+ pos++;
+ // skip trailing space chars before comma or semicolon.
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+ break;
+ } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
+ chars[pos] += 32; //to low case
+ }
+
+ pos++;
+ }
+
+ // verify length of hex string
+ // encoded byte array must be not less then 4 and must be even number
+ int hexLen = end - beg; // skip first '#' char
+ if (hexLen < 5 || (hexLen & 1) == 0) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ // get byte encoding from string representation
+ byte[] encoded = new byte[hexLen / 2];
+ for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
+ encoded[i] = (byte) getByte(p);
+ }
+
+ return new String(chars, beg, hexLen);
+ }
+
+ // gets string attribute value: *( stringchar / pair )
+ private String escapedAV() {
+ beg = pos;
+ end = pos;
+ while (true) {
+ if (pos >= length) {
+ // the end of DN has been found
+ return new String(chars, beg, end - beg);
+ }
+
+ switch (chars[pos]) {
+ case '+':
+ case ',':
+ case ';':
+ // separator char has been found
+ return new String(chars, beg, end - beg);
+ case '\\':
+ // escaped char
+ chars[end++] = getEscaped();
+ pos++;
+ break;
+ case ' ':
+ // need to figure out whether space defines
+ // the end of attribute value or not
+ cur = end;
+
+ pos++;
+ chars[end++] = ' ';
+
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ chars[end++] = ' ';
+ }
+ if (pos == length || chars[pos] == ',' || chars[pos] == '+' || chars[pos] == ';') {
+ // separator char or the end of DN has been found
+ return new String(chars, beg, cur - beg);
+ }
+ break;
+ default:
+ chars[end++] = chars[pos];
+ pos++;
+ }
+ }
+ }
+
+ // returns escaped char
+ private char getEscaped() {
+ pos++;
+ if (pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ switch (chars[pos]) {
+ case '"':
+ case '\\':
+ case ',':
+ case '=':
+ case '+':
+ case '<':
+ case '>':
+ case '#':
+ case ';':
+ case ' ':
+ case '*':
+ case '%':
+ case '_':
+ //FIXME: escaping is allowed only for leading or trailing space char
+ return chars[pos];
+ default:
+ // RFC doesn't explicitly say that escaped hex pair is
+ // interpreted as UTF-8 char. It only contains an example of such DN.
+ return getUTF8();
+ }
+ }
+
+ // decodes UTF-8 char
+ // see http://www.unicode.org for UTF-8 bit distribution table
+ private char getUTF8() {
+ int res = getByte(pos);
+ pos++; //FIXME tmp
+
+ if (res < 128) { // one byte: 0-7F
+ return (char) res;
+ } else if (res >= 192 && res <= 247) {
+
+ int count;
+ if (res <= 223) { // two bytes: C0-DF
+ count = 1;
+ res = res & 0x1F;
+ } else if (res <= 239) { // three bytes: E0-EF
+ count = 2;
+ res = res & 0x0F;
+ } else { // four bytes: F0-F7
+ count = 3;
+ res = res & 0x07;
+ }
+
+ int b;
+ for (int i = 0; i < count; i++) {
+ pos++;
+ if (pos == length || chars[pos] != '\\') {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+ pos++;
+
+ b = getByte(pos);
+ pos++; //FIXME tmp
+ if ((b & 0xC0) != 0x80) {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+
+ res = (res << 6) + (b & 0x3F);
+ }
+ return (char) res;
+ } else {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+ }
+
+ // Returns byte representation of a char pair
+ // The char pair is composed of DN char in
+ // specified 'position' and the next char
+ // According to BNF syntax:
+ // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+ // / "a" / "b" / "c" / "d" / "e" / "f"
+ private int getByte(int position) {
+ if (position + 1 >= length) {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ int b1, b2;
+
+ b1 = chars[position];
+ if (b1 >= '0' && b1 <= '9') {
+ b1 = b1 - '0';
+ } else if (b1 >= 'a' && b1 <= 'f') {
+ b1 = b1 - 87; // 87 = 'a' - 10
+ } else if (b1 >= 'A' && b1 <= 'F') {
+ b1 = b1 - 55; // 55 = 'A' - 10
+ } else {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ b2 = chars[position + 1];
+ if (b2 >= '0' && b2 <= '9') {
+ b2 = b2 - '0';
+ } else if (b2 >= 'a' && b2 <= 'f') {
+ b2 = b2 - 87; // 87 = 'a' - 10
+ } else if (b2 >= 'A' && b2 <= 'F') {
+ b2 = b2 - 55; // 55 = 'A' - 10
+ } else {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ return (b1 << 4) + b2;
+ }
+
+ /**
+ * Parses the DN and returns the most significant attribute value
+ * for an attribute type, or null if none found.
+ *
+ * @param attributeType attribute type to look for (e.g. "ca")
+ */
+ public String findMostSpecific(String attributeType) {
+ // Initialize internal state.
+ pos = 0;
+ beg = 0;
+ end = 0;
+ cur = 0;
+ chars = dn.toCharArray();
+
+ String attType = nextAT();
+ if (attType == null) {
+ return null;
+ }
+ while (true) {
+ String attValue = "";
+
+ if (pos == length) {
+ return null;
+ }
+
+ switch (chars[pos]) {
+ case '"':
+ attValue = quotedAV();
+ break;
+ case '#':
+ attValue = hexAV();
+ break;
+ case '+':
+ case ',':
+ case ';': // compatibility with RFC 1779: semicolon can separate RDNs
+ //empty attribute value
+ break;
+ default:
+ attValue = escapedAV();
+ }
+
+ // Values are ordered from most specific to least specific
+ // due to the RFC2253 formatting. So take the first match
+ // we see.
+ if (attributeType.equalsIgnoreCase(attType)) {
+ return attValue;
+ }
+
+ if (pos >= length) {
+ return null;
+ }
+
+ if (chars[pos] == ',' || chars[pos] == ';') {
+ } else if (chars[pos] != '+') {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ pos++;
+ attType = nextAT();
+ if (attType == null) {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/FinishThread.java b/src/main/java/com/neovisionaries/ws/client/FinishThread.java
new file mode 100644
index 0000000..467c349
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/FinishThread.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class FinishThread extends WebSocketThread {
+ public FinishThread(WebSocket ws) {
+ super("FinishThread", ws, ThreadType.FINISH_THREAD);
+ }
+
+
+ @Override
+ public void runMain() {
+ mWebSocket.finish();
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/FixedDistanceHuffman.java b/src/main/java/com/neovisionaries/ws/client/FixedDistanceHuffman.java
new file mode 100644
index 0000000..89c323e
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/FixedDistanceHuffman.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class FixedDistanceHuffman extends Huffman {
+ private static final FixedDistanceHuffman INSTANCE = new FixedDistanceHuffman();
+
+
+ private FixedDistanceHuffman() {
+ super(buildCodeLensFromSym());
+ }
+
+
+ private static int[] buildCodeLensFromSym() {
+ // 3.2.6. Compression with fixed Huffman codes (BTYPE=01)
+ //
+ // "Distance codes 0-31 are represented by (fixed-length)
+ // 5-bit codes", the specification says.
+
+ int[] codeLengths = new int[32];
+
+ for (int symbol = 0; symbol < 32; ++symbol) {
+ codeLengths[symbol] = 5;
+ }
+
+ // Let Huffman class generate code values from code lengths.
+ // Note that "code lengths are sufficient to generate the
+ // actual codes". See 3.2.2. of RFC 1951.
+ return codeLengths;
+ }
+
+
+ public static FixedDistanceHuffman getInstance() {
+ return INSTANCE;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/FixedLiteralLengthHuffman.java b/src/main/java/com/neovisionaries/ws/client/FixedLiteralLengthHuffman.java
new file mode 100644
index 0000000..b16cecc
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/FixedLiteralLengthHuffman.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class FixedLiteralLengthHuffman extends Huffman {
+ private static final FixedLiteralLengthHuffman INSTANCE = new FixedLiteralLengthHuffman();
+
+
+ private FixedLiteralLengthHuffman() {
+ super(buildCodeLensFromSym());
+ }
+
+
+ private static int[] buildCodeLensFromSym() {
+ // 3.2.6. Compression with fixed Huffman codes (BTYPE=01)
+ //
+ // Lit Value Bits Codes
+ // --------- ---- ---------------------------
+ // 0 - 143 8 00110000 through 10111111
+ // 144 - 255 9 110010000 through 111111111
+ // 256 - 279 7 0000000 through 0010111
+ // 280 - 287 8 11000000 through 11000111
+
+ int[] codeLengths = new int[288];
+
+ int symbol;
+
+ // 0 - 143
+ for (symbol = 0; symbol < 144; ++symbol) {
+ codeLengths[symbol] = 8;
+ }
+
+ // 144 - 255
+ for (; symbol < 256; ++symbol) {
+ codeLengths[symbol] = 9;
+ }
+
+ // 256 - 279
+ for (; symbol < 280; ++symbol) {
+ codeLengths[symbol] = 7;
+ }
+
+ // 280 - 287
+ for (; symbol < 288; ++symbol) {
+ codeLengths[symbol] = 8;
+ }
+
+ // Huffman class generates code values from code lengths.
+ // Note that "code lengths are sufficient to generate the
+ // actual codes". See 3.2.2. of RFC 1951.
+ return codeLengths;
+ }
+
+
+ public static FixedLiteralLengthHuffman getInstance() {
+ return INSTANCE;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/FormatException.java b/src/main/java/com/neovisionaries/ws/client/FormatException.java
new file mode 100644
index 0000000..d3b0295
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/FormatException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class FormatException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+
+ public FormatException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/HandshakeBuilder.java b/src/main/java/com/neovisionaries/ws/client/HandshakeBuilder.java
new file mode 100644
index 0000000..5b304ef
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/HandshakeBuilder.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+
+class HandshakeBuilder {
+ private static final String[] CONNECTION_HEADER = {"Connection", "Upgrade"};
+ private static final String[] UPGRADE_HEADER = {"Upgrade", "websocket"};
+ private static final String[] VERSION_HEADER = {"Sec-WebSocket-Version", "13"};
+ private static final String RN = "\r\n";
+
+
+ private boolean mSecure;
+ private String mUserInfo;
+ private final String mHost;
+ private final String mPath;
+ private final URI mUri;
+ private String mKey;
+ private Set mProtocols;
+ private List mExtensions;
+ private List mHeaders;
+
+
+ public HandshakeBuilder(boolean secure, String userInfo, String host, String path) {
+ mSecure = secure;
+ mUserInfo = userInfo;
+ mHost = host;
+ mPath = path;
+
+ // 'host' may contain ':{port}' at its end.
+ // 'path' may contain '?{query}' at its end.
+ mUri = URI.create(String.format("%s://%s%s", (secure ? "wss" : "ws"), host, path));
+ }
+
+
+ public HandshakeBuilder(HandshakeBuilder source) {
+ mSecure = source.mSecure;
+ mUserInfo = source.mUserInfo;
+ mHost = source.mHost;
+ mPath = source.mPath;
+ mUri = source.mUri;
+ mKey = source.mKey;
+ mProtocols = copyProtocols(source.mProtocols);
+ mExtensions = copyExtensions(source.mExtensions);
+ mHeaders = copyHeaders(source.mHeaders);
+ }
+
+
+ public void addProtocol(String protocol) {
+ if (isValidProtocol(protocol) == false) {
+ throw new IllegalArgumentException("'protocol' must be a non-empty string with characters in the range " + "U+0021 to U+007E not including separator characters.");
+ }
+
+ synchronized (this) {
+ if (mProtocols == null) {
+ // 'LinkedHashSet' is used because the elements
+ // "MUST all be unique strings" and must be
+ // "ordered by preference. See RFC 6455, p18, 10.
+ mProtocols = new LinkedHashSet();
+ }
+
+ mProtocols.add(protocol);
+ }
+ }
+
+
+ public void removeProtocol(String protocol) {
+ if (protocol == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mProtocols == null) {
+ return;
+ }
+
+ mProtocols.remove(protocol);
+
+ if (mProtocols.size() == 0) {
+ mProtocols = null;
+ }
+ }
+ }
+
+
+ public void clearProtocols() {
+ synchronized (this) {
+ mProtocols = null;
+ }
+ }
+
+
+ private static boolean isValidProtocol(String protocol) {
+ if (protocol == null || protocol.length() == 0) {
+ return false;
+ }
+
+ int len = protocol.length();
+
+ for (int i = 0; i < len; ++i) {
+ char ch = protocol.charAt(i);
+
+ if (ch < 0x21 || 0x7E < ch || Token.isSeparator(ch)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ public boolean containsProtocol(String protocol) {
+ synchronized (this) {
+ if (mProtocols == null) {
+ return false;
+ }
+
+ return mProtocols.contains(protocol);
+ }
+ }
+
+
+ public void addExtension(WebSocketExtension extension) {
+ if (extension == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mExtensions == null) {
+ mExtensions = new ArrayList();
+ }
+
+ mExtensions.add(extension);
+ }
+ }
+
+
+ public void addExtension(String extension) {
+ addExtension(WebSocketExtension.parse(extension));
+ }
+
+
+ public void removeExtension(WebSocketExtension extension) {
+ if (extension == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mExtensions == null) {
+ return;
+ }
+
+ mExtensions.remove(extension);
+
+ if (mExtensions.size() == 0) {
+ mExtensions = null;
+ }
+ }
+ }
+
+
+ public void removeExtensions(String name) {
+ if (name == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mExtensions == null) {
+ return;
+ }
+
+ List extensionsToRemove = new ArrayList();
+
+ for (WebSocketExtension extension : mExtensions) {
+ if (extension.getName().equals(name)) {
+ extensionsToRemove.add(extension);
+ }
+ }
+
+ for (WebSocketExtension extension : extensionsToRemove) {
+ mExtensions.remove(extension);
+ }
+
+ if (mExtensions.size() == 0) {
+ mExtensions = null;
+ }
+ }
+ }
+
+
+ public void clearExtensions() {
+ synchronized (this) {
+ mExtensions = null;
+ }
+ }
+
+
+ public boolean containsExtension(WebSocketExtension extension) {
+ if (extension == null) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (mExtensions == null) {
+ return false;
+ }
+
+ return mExtensions.contains(extension);
+ }
+ }
+
+
+ public boolean containsExtension(String name) {
+ if (name == null) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (mExtensions == null) {
+ return false;
+ }
+
+ for (WebSocketExtension extension : mExtensions) {
+ if (extension.getName().equals(name)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+
+ public void addHeader(String name, String value) {
+ if (name == null || name.length() == 0) {
+ return;
+ }
+
+ if (value == null) {
+ value = "";
+ }
+
+ synchronized (this) {
+ if (mHeaders == null) {
+ mHeaders = new ArrayList();
+ }
+
+ mHeaders.add(new String[]{name, value});
+ }
+ }
+
+
+ public void removeHeaders(String name) {
+ if (name == null || name.length() == 0) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mHeaders == null) {
+ return;
+ }
+
+ List headersToRemove = new ArrayList();
+
+ for (String[] header : mHeaders) {
+ if (header[0].equals(name)) {
+ headersToRemove.add(header);
+ }
+ }
+
+ for (String[] header : headersToRemove) {
+ mHeaders.remove(header);
+ }
+
+ if (mHeaders.size() == 0) {
+ mHeaders = null;
+ }
+ }
+ }
+
+
+ public void clearHeaders() {
+ synchronized (this) {
+ mHeaders = null;
+ }
+ }
+
+
+ public void setUserInfo(String userInfo) {
+ synchronized (this) {
+ mUserInfo = userInfo;
+ }
+ }
+
+
+ public void setUserInfo(String id, String password) {
+ if (id == null) {
+ id = "";
+ }
+
+ if (password == null) {
+ password = "";
+ }
+
+ String userInfo = String.format("%s:%s", id, password);
+
+ setUserInfo(userInfo);
+ }
+
+
+ public void clearUserInfo() {
+ synchronized (this) {
+ mUserInfo = null;
+ }
+ }
+
+
+ public URI getURI() {
+ return mUri;
+ }
+
+
+ public void setKey(String key) {
+ mKey = key;
+ }
+
+
+ public String buildRequestLine() {
+ return String.format("GET %s HTTP/1.1", mPath);
+ }
+
+
+ public List buildHeaders() {
+ List headers = new ArrayList();
+
+ // Host
+ headers.add(new String[]{"Host", mHost});
+
+ // Connection
+ headers.add(CONNECTION_HEADER);
+
+ // Upgrade
+ headers.add(UPGRADE_HEADER);
+
+ // Sec-WebSocket-Version
+ headers.add(VERSION_HEADER);
+
+ // Sec-WebSocket-Key
+ headers.add(new String[]{"Sec-WebSocket-Key", mKey});
+
+ // Sec-WebSocket-Protocol
+ if (mProtocols != null && mProtocols.size() != 0) {
+ headers.add(new String[]{"Sec-WebSocket-Protocol", Misc.join(mProtocols, ", ")});
+ }
+
+ // Sec-WebSocket-Extensions
+ if (mExtensions != null && mExtensions.size() != 0) {
+ headers.add(new String[]{"Sec-WebSocket-Extensions", Misc.join(mExtensions, ", ")});
+ }
+
+ // Authorization: Basic
+ if (mUserInfo != null && mUserInfo.length() != 0) {
+ headers.add(new String[]{"Authorization", "Basic " + Base64.encode(mUserInfo)});
+ }
+
+ // Custom headers
+ if (mHeaders != null && mHeaders.size() != 0) {
+ headers.addAll(mHeaders);
+ }
+
+ return headers;
+ }
+
+
+ public static String build(String requestLine, List headers) {
+ StringBuilder builder = new StringBuilder();
+
+ // Append the request line, "GET {path} HTTP/1.1".
+ builder.append(requestLine).append(RN);
+
+ // For each header.
+ for (String[] header : headers) {
+ // Append the header, "{name}: {value}".
+ builder.append(header[0]).append(": ").append(header[1]).append(RN);
+ }
+
+ // Append an empty line.
+ builder.append(RN);
+
+ return builder.toString();
+ }
+
+
+ private static Set copyProtocols(Set protocols) {
+ if (protocols == null) {
+ return null;
+ }
+
+ Set newProtocols = new LinkedHashSet(protocols.size());
+
+ newProtocols.addAll(protocols);
+
+ return newProtocols;
+ }
+
+
+ private static List copyExtensions(List extensions) {
+ if (extensions == null) {
+ return null;
+ }
+
+ List newExtensions = new ArrayList(extensions.size());
+
+ for (WebSocketExtension extension : extensions) {
+ newExtensions.add(new WebSocketExtension(extension));
+ }
+
+ return newExtensions;
+ }
+
+
+ private static List copyHeaders(List headers) {
+ if (headers == null) {
+ return null;
+ }
+
+ List newHeaders = new ArrayList(headers.size());
+
+ for (String[] header : headers) {
+ newHeaders.add(copyHeader(header));
+ }
+
+ return newHeaders;
+ }
+
+
+ private static String[] copyHeader(String[] header) {
+ String[] newHeader = new String[2];
+
+ newHeader[0] = header[0];
+ newHeader[1] = header[1];
+
+ return newHeader;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/HandshakeReader.java b/src/main/java/com/neovisionaries/ws/client/HandshakeReader.java
new file mode 100644
index 0000000..4fa80d4
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/HandshakeReader.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Reader for a WebSocket opening handshake response.
+ *
+ * @since 1.19
+ */
+class HandshakeReader {
+ private static final String ACCEPT_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ private final WebSocket mWebSocket;
+
+
+ public HandshakeReader(WebSocket websocket) {
+ mWebSocket = websocket;
+ }
+
+
+ public Map> readHandshake(WebSocketInputStream input, String key) throws WebSocketException {
+ // Read the status line.
+ StatusLine statusLine = readStatusLine(input);
+
+ // Read HTTP headers.
+ Map> headers = readHttpHeaders(input);
+
+ // Validate the status line.
+ validateStatusLine(statusLine, headers, input);
+
+ // Validate the value of Upgrade.
+ validateUpgrade(statusLine, headers);
+
+ // Validate the value of Connection.
+ validateConnection(statusLine, headers);
+
+ // Validate the value of Sec-WebSocket-Accept.
+ validateAccept(statusLine, headers, key);
+
+ // Validate the value of Sec-WebSocket-Extensions.
+ validateExtensions(statusLine, headers);
+
+ // Validate the value of Sec-WebSocket-Protocol.
+ validateProtocol(statusLine, headers);
+
+ // OK. The server has accepted the web socket request.
+ return headers;
+ }
+
+
+ /**
+ * Read a status line from an HTTP server.
+ */
+ private StatusLine readStatusLine(WebSocketInputStream input) throws WebSocketException {
+ String line;
+
+ try {
+ // Read the status line.
+ line = input.readLine();
+ } catch (IOException e) {
+ // Failed to read an opening handshake response from the server.
+ throw new WebSocketException(WebSocketError.OPENING_HANDSHAKE_RESPONSE_FAILURE, "Failed to read an opening handshake response from the server: " + e.getMessage(), e);
+ }
+
+ if (line == null || line.length() == 0) {
+ // The status line of the opening handshake response is empty.
+ throw new WebSocketException(WebSocketError.STATUS_LINE_EMPTY, "The status line of the opening handshake response is empty.");
+ }
+
+ try {
+ // Parse the status line.
+ return new StatusLine(line);
+ } catch (Exception e) {
+ // The status line of the opening handshake response is badly formatted.
+ throw new WebSocketException(WebSocketError.STATUS_LINE_BAD_FORMAT, "The status line of the opening handshake response is badly formatted. The status line is: " + line);
+ }
+ }
+
+
+ private Map> readHttpHeaders(WebSocketInputStream input) throws WebSocketException {
+ // Create a map of HTTP headers. Keys are case-insensitive.
+ Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER);
+
+ StringBuilder builder = null;
+ String line;
+
+ while (true) {
+ try {
+ line = input.readLine();
+ } catch (IOException e) {
+ // An error occurred while HTTP header section was being read.
+ throw new WebSocketException(WebSocketError.HTTP_HEADER_FAILURE, "An error occurred while HTTP header section was being read: " + e.getMessage(), e);
+ }
+
+ // If the end of the header section was reached.
+ if (line == null || line.length() == 0) {
+ if (builder != null) {
+ parseHttpHeader(headers, builder.toString());
+ }
+
+ // The end of the header section.
+ break;
+ }
+
+ // The first line of the line.
+ char ch = line.charAt(0);
+
+ // If the first char is SP or HT.
+ if (ch == ' ' || ch == '\t') {
+ if (builder == null) {
+ // Weird. No preceding "field-name:field-value" line. Ignore this line.
+ continue;
+ }
+
+ // Replacing the leading 1*(SP|HT) to a single SP.
+ line = line.replaceAll("^[ \t]+", " ");
+
+ // Concatenate
+ builder.append(line);
+
+ continue;
+ }
+
+ if (builder != null) {
+ parseHttpHeader(headers, builder.toString());
+ }
+
+ builder = new StringBuilder(line);
+ }
+
+ return headers;
+ }
+
+
+ private void parseHttpHeader(Map> headers, String header) {
+ // Split 'header' to name & value.
+ String[] pair = header.split(":", 2);
+
+ if (pair.length < 2) {
+ // Weird. Ignore this header.
+ return;
+ }
+
+ // Name. (Remove leading and trailing spaces)
+ String name = pair[0].trim();
+
+ // Value. (Remove leading and trailing spaces)
+ String value = pair[1].trim();
+
+ List list = headers.get(name);
+
+ if (list == null) {
+ list = new ArrayList();
+ headers.put(name, list);
+ }
+
+ list.add(value);
+ }
+
+
+ /**
+ * Validate the status line. {@code "101 Switching Protocols"} is expected.
+ */
+ private void validateStatusLine(StatusLine statusLine, Map> headers, WebSocketInputStream input) throws WebSocketException {
+ // If the status code is 101 (Switching Protocols).
+ if (statusLine.getStatusCode() == 101) {
+ // OK. The server can speak the WebSocket protocol.
+ return;
+ }
+
+ // Read the response body.
+ byte[] body = readBody(headers, input);
+
+ // The status code of the opening handshake response is not Switching Protocols.
+ throw new OpeningHandshakeException(WebSocketError.NOT_SWITCHING_PROTOCOLS, "The status code of the opening handshake response is not '101 Switching Protocols'. The status line is: " + statusLine, statusLine, headers, body);
+ }
+
+
+ /**
+ * Read the response body
+ */
+ private byte[] readBody(Map> headers, WebSocketInputStream input) {
+ // Get the value of "Content-Length" header.
+ int length = getContentLength(headers);
+
+ if (length <= 0) {
+ // Response body is not available.
+ return null;
+ }
+
+ try {
+ // Allocate a byte array of the content length.
+ byte[] body = new byte[length];
+
+ // Read the response body into the byte array.
+ input.readBytes(body, length);
+
+ // Return the content of the response body.
+ return body;
+ } catch (Throwable t) {
+ // Response body is not available.
+ return null;
+ }
+ }
+
+
+ /**
+ * Get the value of "Content-Length" header.
+ */
+ private int getContentLength(Map> headers) {
+ try {
+ return Integer.parseInt(headers.get("Content-Length").get(0));
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+
+ /**
+ * Validate the value of {@code Upgrade} header.
+ *
+ *
+ *
From RFC 6455, p19.
+ *
+ * If the response lacks an {@code Upgrade} header field or the {@code Upgrade}
+ * header field contains a value that is not an ASCII case-insensitive match for
+ * the value "websocket", the client MUST Fail the WebSocket Connection.
+ *
+ *
+ */
+ private void validateUpgrade(StatusLine statusLine, Map> headers) throws WebSocketException {
+ // Get the values of Upgrade.
+ List values = headers.get("Upgrade");
+
+ if (values == null || values.size() == 0) {
+ // The opening handshake response does not contain 'Upgrade' header.
+ throw new OpeningHandshakeException(WebSocketError.NO_UPGRADE_HEADER, "The opening handshake response does not contain 'Upgrade' header.", statusLine, headers);
+ }
+
+ for (String value : values) {
+ // Split the value of Upgrade header into elements.
+ String[] elements = value.split("\\s*,\\s*");
+
+ for (String element : elements) {
+ if ("websocket".equalsIgnoreCase(element)) {
+ // Found 'websocket' in Upgrade header.
+ return;
+ }
+ }
+ }
+
+ // 'websocket' was not found in 'Upgrade' header.
+ throw new OpeningHandshakeException(WebSocketError.NO_WEBSOCKET_IN_UPGRADE_HEADER, "'websocket' was not found in 'Upgrade' header.", statusLine, headers);
+ }
+
+
+ /**
+ * Validate the value of {@code Connection} header.
+ *
+ *
+ *
From RFC 6455, p19.
+ *
+ * If the response lacks a {@code Connection} header field or the {@code Connection}
+ * header field doesn't contain a token that is an ASCII case-insensitive match
+ * for the value "Upgrade", the client MUST Fail the WebSocket Connection.
+ *
+ *
+ */
+ private void validateConnection(StatusLine statusLine, Map> headers) throws WebSocketException {
+ // Get the values of Upgrade.
+ List values = headers.get("Connection");
+
+ if (values == null || values.size() == 0) {
+ // The opening handshake response does not contain 'Connection' header.
+ throw new OpeningHandshakeException(WebSocketError.NO_CONNECTION_HEADER, "The opening handshake response does not contain 'Connection' header.", statusLine, headers);
+ }
+
+ for (String value : values) {
+ // Split the value of Connection header into elements.
+ String[] elements = value.split("\\s*,\\s*");
+
+ for (String element : elements) {
+ if ("Upgrade".equalsIgnoreCase(element)) {
+ // Found 'Upgrade' in Connection header.
+ return;
+ }
+ }
+ }
+
+ // 'Upgrade' was not found in 'Connection' header.
+ throw new OpeningHandshakeException(WebSocketError.NO_UPGRADE_IN_CONNECTION_HEADER, "'Upgrade' was not found in 'Connection' header.", statusLine, headers);
+ }
+
+
+ /**
+ * Validate the value of {@code Sec-WebSocket-Accept} header.
+ *
+ *
+ *
From RFC 6455, p19.
+ *
+ * If the response lacks a {@code Sec-WebSocket-Accept} header field or the
+ * {@code Sec-WebSocket-Accept} contains a value other than the base64-encoded
+ * SHA-1 of the concatenation of the {@code Sec-WebSocket-Key} (as a string,
+ * not base64-decoded) with the string "{@code 258EAFA5-E914-47DA-95CA-C5AB0DC85B11}"
+ * but ignoring any leading and trailing whitespace, the client MUST Fail the
+ * WebSocket Connection.
+ *
+ *
+ */
+ private void validateAccept(StatusLine statusLine, Map> headers, String key) throws WebSocketException {
+ // Get the values of Sec-WebSocket-Accept.
+ List values = headers.get("Sec-WebSocket-Accept");
+
+ if (values == null) {
+ // The opening handshake response does not contain 'Sec-WebSocket-Accept' header.
+ throw new OpeningHandshakeException(WebSocketError.NO_SEC_WEBSOCKET_ACCEPT_HEADER, "The opening handshake response does not contain 'Sec-WebSocket-Accept' header.", statusLine, headers);
+ }
+
+ // The actual value of Sec-WebSocket-Accept.
+ String actual = values.get(0);
+
+ // Concatenate.
+ String input = key + ACCEPT_MAGIC;
+
+ // Expected value of Sec-WebSocket-Accept
+ String expected;
+
+ try {
+ // Message digest for SHA-1.
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+
+ // Compute the digest value.
+ byte[] digest = md.digest(Misc.getBytesUTF8(input));
+
+ // Base64.
+ expected = Base64.encode(digest);
+ } catch (Exception e) {
+ // This never happens.
+ return;
+ }
+
+ if (expected.equals(actual) == false) {
+ // The value of 'Sec-WebSocket-Accept' header is different from the expected one.
+ throw new OpeningHandshakeException(WebSocketError.UNEXPECTED_SEC_WEBSOCKET_ACCEPT_HEADER, "The value of 'Sec-WebSocket-Accept' header is different from the expected one.", statusLine, headers);
+ }
+
+ // OK. The value of Sec-WebSocket-Accept is the same as the expected one.
+ }
+
+
+ /**
+ * Validate the value of {@code Sec-WebSocket-Extensions} header.
+ *
+ *
+ *
From RFC 6455, p19.
+ *
+ * If the response includes a {@code Sec-WebSocket-Extensions} header
+ * field and this header field indicates the use of an extension
+ * that was not present in the client's handshake (the server has
+ * indicated an extension not requested by the client), the client
+ * MUST Fail the WebSocket Connection.
+ *
+ *
+ */
+ private void validateExtensions(StatusLine statusLine, Map> headers) throws WebSocketException {
+ // Get the values of Sec-WebSocket-Extensions.
+ List values = headers.get("Sec-WebSocket-Extensions");
+
+ if (values == null || values.size() == 0) {
+ // Nothing to check.
+ return;
+ }
+
+ List extensions = new ArrayList();
+
+ for (String value : values) {
+ // Split the value into elements each of which represents an extension.
+ String[] elements = value.split("\\s*,\\s*");
+
+ for (String element : elements) {
+ // Parse the string whose format is supposed to be "{name}[; {key}[={value}]*".
+ WebSocketExtension extension = WebSocketExtension.parse(element);
+
+ if (extension == null) {
+ // The value in 'Sec-WebSocket-Extensions' failed to be parsed.
+ throw new OpeningHandshakeException(WebSocketError.EXTENSION_PARSE_ERROR, "The value in 'Sec-WebSocket-Extensions' failed to be parsed: " + element, statusLine, headers);
+ }
+
+ // The extension name.
+ String name = extension.getName();
+
+ // If the extension is not contained in the original request from this client.
+ if (mWebSocket.getHandshakeBuilder().containsExtension(name) == false) {
+ // The extension contained in the Sec-WebSocket-Extensions header is not supported.
+ throw new OpeningHandshakeException(WebSocketError.UNSUPPORTED_EXTENSION, "The extension contained in the Sec-WebSocket-Extensions header is not supported: " + name, statusLine, headers);
+ }
+
+ // Let the extension validate itself.
+ extension.validate();
+
+ // The extension has been agreed.
+ extensions.add(extension);
+ }
+ }
+
+ // Check if extensions conflict with each other.
+ validateExtensionCombination(statusLine, headers, extensions);
+
+ // Agreed extensions.
+ mWebSocket.setAgreedExtensions(extensions);
+ }
+
+
+ private void validateExtensionCombination(StatusLine statusLine, Map> headers, List extensions) throws WebSocketException {
+ // Currently, only duplication of per-message compression extensions is checked.
+
+ // A per-message compression extension found in the list.
+ WebSocketExtension pmce = null;
+
+ for (WebSocketExtension extension : extensions) {
+ // If the extension is not a per-message compression extension.
+ if ((extension instanceof PerMessageCompressionExtension) == false) {
+ continue;
+ }
+
+ // If the found per-message compression extension is the first one.
+ if (pmce == null) {
+ // Found a per-message compression extension.
+ pmce = extension;
+ continue;
+ }
+
+ // Found the second per-message compression extension. Conflict.
+ String message = String.format("'%s' extension and '%s' extension conflict with each other.", pmce.getName(), extension.getName());
+
+ // The extensions conflict with each other.
+ throw new OpeningHandshakeException(
+ WebSocketError.EXTENSIONS_CONFLICT, message, statusLine, headers);
+ }
+ }
+
+
+ /**
+ * Validate the value of {@code Sec-WebSocket-Protocol} header.
+ *
+ *
+ *
From RFC 6455, p20.
+ *
+ * If the response includes a {@code Sec-WebSocket-Protocol} header field
+ * and this header field indicates the use of a subprotocol that was
+ * not present in the client's handshake (the server has indicated a
+ * subprotocol not requested by the client), the client MUST Fail
+ * the WebSocket Connection.
+ *
+ *
+ */
+ private void validateProtocol(StatusLine statusLine, Map> headers) throws WebSocketException {
+ // Get the values of Sec-WebSocket-Protocol.
+ List values = headers.get("Sec-WebSocket-Protocol");
+
+ if (values == null) {
+ // Nothing to check.
+ return;
+ }
+
+ // Protocol
+ String protocol = values.get(0);
+
+ if (protocol == null || protocol.length() == 0) {
+ // Ignore.
+ return;
+ }
+
+ // If the protocol is not contained in the original request
+ // from this client.
+ if (mWebSocket.getHandshakeBuilder().containsProtocol(protocol) == false) {
+ // The protocol contained in the Sec-WebSocket-Protocol header is not supported.
+ throw new OpeningHandshakeException(WebSocketError.UNSUPPORTED_PROTOCOL, "The protocol contained in the Sec-WebSocket-Protocol header is not supported: " + protocol, statusLine, headers);
+ }
+
+ // Agreed protocol.
+ mWebSocket.setAgreedProtocol(protocol);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/HostnameUnverifiedException.java b/src/main/java/com/neovisionaries/ws/client/HostnameUnverifiedException.java
new file mode 100644
index 0000000..b837c6f
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/HostnameUnverifiedException.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import javax.net.ssl.SSLSocket;
+
+
+/**
+ * The certificate of the peer does not match the expected hostname.
+ *
+ *
+ * {@link #getError()} of this class returns {@link WebSocketError#HOSTNAME_UNVERIFIED
+ * HOSTNAME_UNVERIFIED}.
+ *
+ *
+ * @since 2.1
+ */
+public class HostnameUnverifiedException extends WebSocketException {
+ private static final long serialVersionUID = 1L;
+
+
+ private final SSLSocket mSSLSocket;
+ private final String mHostname;
+
+
+ /**
+ * Constructor with the SSL socket and the expected hostname.
+ *
+ * @param socket The SSL socket against which the hostname verification failed.
+ * @param hostname The expected hostname.
+ */
+ public HostnameUnverifiedException(SSLSocket socket, String hostname) {
+ super(WebSocketError.HOSTNAME_UNVERIFIED, String.format("The certificate of the peer%s does not match the expected hostname (%s)", stringifyPrincipal(socket), hostname));
+
+ mSSLSocket = socket;
+ mHostname = hostname;
+ }
+
+
+ private static String stringifyPrincipal(SSLSocket socket) {
+ try {
+ return String.format(" (%s)", socket.getSession().getPeerPrincipal().toString());
+ } catch (Exception e) {
+ // Principal information is not available.
+ return "";
+ }
+ }
+
+
+ /**
+ * Get the SSL socket against which the hostname verification failed.
+ *
+ * @return The SSL socket.
+ */
+ public SSLSocket getSSLSocket() {
+ return mSSLSocket;
+ }
+
+
+ /**
+ * Get the expected hostname.
+ *
+ * @return The expected hostname.
+ */
+ public String getHostname() {
+ return mHostname;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/Huffman.java b/src/main/java/com/neovisionaries/ws/client/Huffman.java
new file mode 100644
index 0000000..ac8b0cc
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Huffman.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Huffman coding for DEFLATE format (RFC 1951).
+ */
+class Huffman {
+ private final int mMinCodeLen;
+ private final int mMaxCodeLen;
+ private final int[] mMaxCodeValsFromCodeLen;
+ private final int[] mSymsFromCodeVal;
+
+
+ public Huffman(int[] codeLensFromSym) {
+ // Remember the minimum and maximum code lengths.
+ mMinCodeLen = Math.max(Misc.min(codeLensFromSym), 1);
+ mMaxCodeLen = Misc.max(codeLensFromSym);
+
+ // Count the number of entries for each code length.
+ int[] countsFromCodeLen = createCountsFromCodeLen(codeLensFromSym, mMaxCodeLen);
+
+ // Create an array holding the maximum code values for each code length.
+ Object[] out = new Object[2];
+ mMaxCodeValsFromCodeLen = createMaxCodeValsFromCodeLen(countsFromCodeLen, mMaxCodeLen, out);
+
+ // Create a table to convert code values int symbols.
+ int[] codeValsFromCodeLen = (int[]) out[0];
+ int maxCodeVal = ((Integer) out[1]).intValue();
+ mSymsFromCodeVal = createSymsFromCodeVal(codeLensFromSym, codeValsFromCodeLen, maxCodeVal);
+ }
+
+
+ /**
+ * Create an array whose elements have the given initial value.
+ */
+ private static int[] createIntArray(int size, int initialValue) {
+ int[] array = new int[size];
+
+ for (int i = 0; i < size; ++i) {
+ array[i] = initialValue;
+ }
+
+ return array;
+ }
+
+
+ private static int[] createCountsFromCodeLen(int[] codeLensFromSym, int maxCodeLen) {
+ int[] countsFromCodeLen = new int[maxCodeLen + 1];
+
+ // Count the number of entries for each code length.
+ // This corresponds to the step 1 in 3.2.2. of RFC 1951.
+ for (int symbol = 0; symbol < codeLensFromSym.length; ++symbol) {
+ int codeLength = codeLensFromSym[symbol];
+ ++countsFromCodeLen[codeLength];
+ }
+
+ return countsFromCodeLen;
+ }
+
+
+ private static int[] createMaxCodeValsFromCodeLen(int[] countsFromCodeLen, int maxCodeLen, Object[] out) {
+ // Initialize an array that holds the maximum code values
+ // for each code length. '-1' indicates that there is no
+ // code value for the code length.
+ int[] maxCodeValsFromCodeLen = createIntArray(maxCodeLen + 1, -1);
+
+ // Compute the smallest code value for each code length.
+ // This corresponds to the step 2 in 3.2.2. of RFC 1951.
+ int minCodeVal = 0;
+ int maxCodeVal = 0;
+ countsFromCodeLen[0] = 0;
+ int[] codeValsFromCodeLen = new int[maxCodeLen + 1];
+ for (int codeLen = 1; codeLen < countsFromCodeLen.length; ++codeLen) {
+ // Compute the minimum code value for each code length.
+ int prevCount = countsFromCodeLen[codeLen - 1];
+ minCodeVal = (minCodeVal + prevCount) << 1;
+ codeValsFromCodeLen[codeLen] = minCodeVal;
+
+ // Compute the maximum code value for each code length.
+ maxCodeVal = minCodeVal + countsFromCodeLen[codeLen] - 1;
+ maxCodeValsFromCodeLen[codeLen] = maxCodeVal;
+ }
+
+ out[0] = codeValsFromCodeLen;
+ out[1] = Integer.valueOf(maxCodeVal);
+
+ return maxCodeValsFromCodeLen;
+ }
+
+
+ private static int[] createSymsFromCodeVal(int[] codeLensFromSym, int[] codeValsFromCodeLen, int maxCodeVal) {
+ int[] symsFromCodeVal = new int[maxCodeVal + 1];
+
+ // Set up a table to convert code values into symbols.
+ // This corresponds to the step 3 in 3.2.2. of RFC 1951.
+
+ for (int sym = 0; sym < codeLensFromSym.length; ++sym) {
+ int codeLen = codeLensFromSym[sym];
+
+ if (codeLen == 0) {
+ continue;
+ }
+
+ int codeVal = codeValsFromCodeLen[codeLen]++;
+ symsFromCodeVal[codeVal] = sym;
+ }
+
+ return symsFromCodeVal;
+ }
+
+
+ public int readSym(ByteArray data, int[] bitIndex) throws FormatException {
+ for (int codeLen = mMinCodeLen; codeLen <= mMaxCodeLen; ++codeLen) {
+ // Get the maximum one from among the code values
+ // whose code length is 'codeLen'.
+ int maxCodeVal = mMaxCodeValsFromCodeLen[codeLen];
+
+ if (maxCodeVal < 0) {
+ // There is no code value whose code length is 'codeLen'.
+ continue;
+ }
+
+ // Read a code value from the input assuming its code length is 'codeLen'.
+ int codeVal = data.getHuffmanBits(bitIndex[0], codeLen);
+
+ if (maxCodeVal < codeVal) {
+ // The read code value is bigger than the maximum code value
+ // among the code values whose code length is 'codeLen'.
+ //
+ // Considering the latter rule of the two added for DEFLATE format
+ // (3.2.2. Use of Huffman coding in the "deflate" format),
+ //
+ // * All codes of a given bit length have lexicographically
+ // consecutive values, in the same order as the symbols
+ // they represent;
+ //
+ // * Shorter codes lexicographically precede longer codes.
+ //
+ // We can expect that the code length of the code value we are
+ // parsing is longer than the current 'codeLen'.
+ continue;
+ }
+
+ // Convert the code value into a symbol value.
+ int sym = mSymsFromCodeVal[codeVal];
+
+ // Consume the bits of the code value.
+ bitIndex[0] += codeLen;
+
+ return sym;
+ }
+
+ // Bad code at the bit index.
+ String message = String.format(
+ "[%s] Bad code at the bit index '%d'.",
+ getClass().getSimpleName(), bitIndex[0]);
+
+ throw new FormatException(message);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/InsufficientDataException.java b/src/main/java/com/neovisionaries/ws/client/InsufficientDataException.java
new file mode 100644
index 0000000..7e51d9d
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/InsufficientDataException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class InsufficientDataException extends WebSocketException {
+ private static final long serialVersionUID = 1L;
+
+
+ private final int mRequestedByteCount;
+ private final int mReadByteCount;
+
+
+ public InsufficientDataException(int requestedByteCount, int readByteCount) {
+ super(WebSocketError.INSUFFICENT_DATA, "The end of the stream has been reached unexpectedly.");
+
+ mRequestedByteCount = requestedByteCount;
+ mReadByteCount = readByteCount;
+ }
+
+
+ public int getRequestedByteCount() {
+ return mRequestedByteCount;
+ }
+
+
+ public int getReadByteCount() {
+ return mReadByteCount;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ListenerManager.java b/src/main/java/com/neovisionaries/ws/client/ListenerManager.java
new file mode 100644
index 0000000..196d7d1
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ListenerManager.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+
+class ListenerManager {
+ private final WebSocket mWebSocket;
+ private final List mListeners = new ArrayList();
+
+ private boolean mSyncNeeded = true;
+ private List mCopiedListeners;
+
+
+ public ListenerManager(WebSocket websocket) {
+ mWebSocket = websocket;
+ }
+
+
+ public List getListeners() {
+ return mListeners;
+ }
+
+
+ public void addListener(WebSocketListener listener) {
+ if (listener == null) {
+ return;
+ }
+
+ synchronized (mListeners) {
+ mListeners.add(listener);
+ mSyncNeeded = true;
+ }
+ }
+
+
+ public void addListeners(List listeners) {
+ if (listeners == null) {
+ return;
+ }
+
+ synchronized (mListeners) {
+ for (WebSocketListener listener : listeners) {
+ if (listener == null) {
+ continue;
+ }
+
+ mListeners.add(listener);
+ mSyncNeeded = true;
+ }
+ }
+ }
+
+
+ public void removeListener(WebSocketListener listener) {
+ if (listener == null) {
+ return;
+ }
+
+ synchronized (mListeners) {
+ if (mListeners.remove(listener)) {
+ mSyncNeeded = true;
+ }
+ }
+ }
+
+
+ public void removeListeners(List listeners) {
+ if (listeners == null) {
+ return;
+ }
+
+ synchronized (mListeners) {
+ for (WebSocketListener listener : listeners) {
+ if (listener == null) {
+ continue;
+ }
+
+ if (mListeners.remove(listener)) {
+ mSyncNeeded = true;
+ }
+ }
+ }
+ }
+
+
+ public void clearListeners() {
+ synchronized (mListeners) {
+ if (mListeners.size() == 0) {
+ return;
+ }
+
+ mListeners.clear();
+ mSyncNeeded = true;
+ }
+ }
+
+
+ private List getSynchronizedListeners() {
+ synchronized (mListeners) {
+ if (mSyncNeeded == false) {
+ return mCopiedListeners;
+ }
+
+ // Copy mListeners to copiedListeners.
+ List copiedListeners =
+ new ArrayList(mListeners.size());
+
+ for (WebSocketListener listener : mListeners) {
+ copiedListeners.add(listener);
+ }
+
+ // Synchronize.
+ mCopiedListeners = copiedListeners;
+ mSyncNeeded = false;
+
+ return copiedListeners;
+ }
+ }
+
+
+ public void callOnStateChanged(WebSocketState newState) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onStateChanged(mWebSocket, newState);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnConnected(Map> headers) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onConnected(mWebSocket, headers);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnConnectError(WebSocketException cause) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onConnectError(mWebSocket, cause);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnDisconnected(WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onDisconnected(mWebSocket, serverCloseFrame, clientCloseFrame, closedByServer);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnContinuationFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onContinuationFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnTextFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onTextFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnBinaryFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onBinaryFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnCloseFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onCloseFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnPingFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onPingFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnPongFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onPongFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnTextMessage(String message) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onTextMessage(mWebSocket, message);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnBinaryMessage(byte[] message) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onBinaryMessage(mWebSocket, message);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnSendingFrame(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onSendingFrame(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnFrameSent(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onFrameSent(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnFrameUnsent(WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onFrameUnsent(mWebSocket, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnThreadCreated(ThreadType threadType, Thread thread) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onThreadCreated(mWebSocket, threadType, thread);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnThreadStarted(ThreadType threadType, Thread thread) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onThreadStarted(mWebSocket, threadType, thread);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnThreadStopping(ThreadType threadType, Thread thread) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onThreadStopping(mWebSocket, threadType, thread);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnError(WebSocketException cause) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onError(mWebSocket, cause);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnFrameError(WebSocketException cause, WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onFrameError(mWebSocket, cause, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnMessageError(WebSocketException cause, List frames) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onMessageError(mWebSocket, cause, frames);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnMessageDecompressionError(WebSocketException cause, byte[] compressed) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onMessageDecompressionError(mWebSocket, cause, compressed);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnTextMessageError(WebSocketException cause, byte[] data) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onTextMessageError(mWebSocket, cause, data);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnSendError(WebSocketException cause, WebSocketFrame frame) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onSendError(mWebSocket, cause, frame);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ public void callOnUnexpectedError(WebSocketException cause) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onUnexpectedError(mWebSocket, cause);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+
+
+ private void callHandleCallbackError(WebSocketListener listener, Throwable cause) {
+ try {
+ listener.handleCallbackError(mWebSocket, cause);
+ } catch (Throwable t) {
+ }
+ }
+
+
+ public void callOnSendingHandshake(String requestLine, List headers) {
+ for (WebSocketListener listener : getSynchronizedListeners()) {
+ try {
+ listener.onSendingHandshake(mWebSocket, requestLine, headers);
+ } catch (Throwable t) {
+ callHandleCallbackError(listener, t);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/Misc.java b/src/main/java/com/neovisionaries/ws/client/Misc.java
new file mode 100644
index 0000000..65ccf98
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Misc.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.SecureRandom;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.neovisionaries.ws.client.WebSocketOpcode.BINARY;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CLOSE;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CONTINUATION;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PING;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PONG;
+import static com.neovisionaries.ws.client.WebSocketOpcode.TEXT;
+
+
+class Misc {
+ private static final SecureRandom sRandom = new SecureRandom();
+
+
+ private Misc() {
+ }
+
+
+ /**
+ * Get a UTF-8 byte array representation of the given string.
+ */
+ public static byte[] getBytesUTF8(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ try {
+ return string.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // This never happens.
+ return null;
+ }
+ }
+
+
+ /**
+ * Convert a UTF-8 byte array into a string.
+ */
+ public static String toStringUTF8(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ return toStringUTF8(bytes, 0, bytes.length);
+ }
+
+
+ /**
+ * Convert a UTF-8 byte array into a string.
+ */
+ public static String toStringUTF8(byte[] bytes, int offset, int length) {
+ if (bytes == null) {
+ return null;
+ }
+
+ try {
+ return new String(bytes, offset, length, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // This never happens.
+ return null;
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Fill the given buffer with random bytes.
+ */
+ public static byte[] nextBytes(byte[] buffer) {
+ sRandom.nextBytes(buffer);
+
+ return buffer;
+ }
+
+
+ /**
+ * Create a buffer of the given size filled with random bytes.
+ */
+ public static byte[] nextBytes(int nBytes) {
+ byte[] buffer = new byte[nBytes];
+
+ return nextBytes(buffer);
+ }
+
+
+ /**
+ * Convert a WebSocket opcode into a string representation.
+ */
+ public static String toOpcodeName(int opcode) {
+ switch (opcode) {
+ case CONTINUATION:
+ return "CONTINUATION";
+
+ case TEXT:
+ return "TEXT";
+
+ case BINARY:
+ return "BINARY";
+
+ case CLOSE:
+ return "CLOSE";
+
+ case PING:
+ return "PING";
+
+ case PONG:
+ return "PONG";
+
+ default:
+ break;
+ }
+
+ if (0x1 <= opcode && opcode <= 0x7) {
+ return String.format("DATA(0x%X)", opcode);
+ }
+
+ if (0x8 <= opcode && opcode <= 0xF) {
+ return String.format("CONTROL(0x%X)", opcode);
+ }
+
+ return String.format("0x%X", opcode);
+ }
+
+
+ /**
+ * Read a line from the given stream.
+ */
+ public static String readLine(InputStream in, String charset) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ while (true) {
+ // Read one byte from the stream.
+ int b = in.read();
+
+ // If the end of the stream was reached.
+ if (b == -1) {
+ if (baos.size() == 0) {
+ // No more line.
+ return null;
+ } else {
+ // The end of the line was reached.
+ break;
+ }
+ }
+
+ if (b == '\n') {
+ // The end of the line was reached.
+ break;
+ }
+
+ if (b != '\r') {
+ // Normal character.
+ baos.write(b);
+ continue;
+ }
+
+ // Read one more byte.
+ int b2 = in.read();
+
+ // If the end of the stream was reached.
+ if (b2 == -1) {
+ // Treat the '\r' as a normal character.
+ baos.write(b);
+
+ // The end of the line was reached.
+ break;
+ }
+
+ // If '\n' follows the '\r'.
+ if (b2 == '\n') {
+ // The end of the line was reached.
+ break;
+ }
+
+ // Treat the '\r' as a normal character.
+ baos.write(b);
+
+ // Append the byte which follows the '\r'.
+ baos.write(b2);
+ }
+
+ // Convert the byte array to a string.
+ return baos.toString(charset);
+ }
+
+
+ /**
+ * Find the minimum value from the given array.
+ */
+ public static int min(int[] values) {
+ int min = Integer.MAX_VALUE;
+
+ for (int i = 0; i < values.length; ++i) {
+ if (values[i] < min) {
+ min = values[i];
+ }
+ }
+
+ return min;
+ }
+
+
+ /**
+ * Find the maximum value from the given array.
+ */
+ public static int max(int[] values) {
+ int max = Integer.MIN_VALUE;
+
+ for (int i = 0; i < values.length; ++i) {
+ if (max < values[i]) {
+ max = values[i];
+ }
+ }
+
+ return max;
+ }
+
+
+ public static String join(Collection> values, String delimiter) {
+ StringBuilder builder = new StringBuilder();
+
+ join(builder, values, delimiter);
+
+ return builder.toString();
+ }
+
+
+ private static void join(StringBuilder builder, Collection> values, String delimiter) {
+ boolean first = true;
+
+ for (Object value : values) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(delimiter);
+ }
+
+ builder.append(value.toString());
+ }
+ }
+
+
+ public static String extractHost(URI uri) {
+ // Extract the host part from the URI.
+ String host = uri.getHost();
+
+ if (host != null) {
+ return host;
+ }
+
+ // According to Issue#74, URI.getHost() method returns null in
+ // the following environment when the host part of the URI is
+ // a host name.
+ //
+ // - Samsung Galaxy S3 + Android API 18
+ // - Samsung Galaxy S4 + Android API 21
+ //
+ // The following is a workaround for the issue.
+
+ // Extract the host part from the authority part of the URI.
+ host = extractHostFromAuthorityPart(uri.getRawAuthority());
+
+ if (host != null) {
+ return host;
+ }
+
+ // Extract the host part from the entire URI.
+ return extractHostFromEntireUri(uri.toString());
+ }
+
+
+ static String extractHostFromAuthorityPart(String authority) {
+ // If the authority part is not available.
+ if (authority == null) {
+ // Hmm... This should not happen.
+ return null;
+ }
+
+ // Parse the authority part. The expected format is "[id:password@]host[:port]".
+ Matcher matcher = Pattern.compile("^(.*@)?([^:]+)(:\\d+)?$").matcher(authority);
+
+ // If the authority part does not match the expected format.
+ if (matcher == null || matcher.matches() == false) {
+ // Hmm... This should not happen.
+ return null;
+ }
+
+ // Return the host part.
+ return matcher.group(2);
+ }
+
+
+ static String extractHostFromEntireUri(String uri) {
+ if (uri == null) {
+ // Hmm... This should not happen.
+ return null;
+ }
+
+ // Parse the URI. The expected format is "scheme://[id:password@]host[:port][...]".
+ Matcher matcher = Pattern.compile("^\\w+://([^@/]*@)?([^:/]+)(:\\d+)?(/.*)?$").matcher(uri);
+
+ // If the URI does not match the expected format.
+ if (matcher == null || matcher.matches() == false) {
+ // Hmm... This should not happen.
+ return null;
+ }
+
+ // Return the host part.
+ return matcher.group(2);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/NoMoreFrameException.java b/src/main/java/com/neovisionaries/ws/client/NoMoreFrameException.java
new file mode 100644
index 0000000..0bbd259
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/NoMoreFrameException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class NoMoreFrameException extends WebSocketException {
+ private static final long serialVersionUID = 1L;
+
+
+ public NoMoreFrameException() {
+ super(WebSocketError.NO_MORE_FRAME, "No more WebSocket frame from the server.");
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/OkHostnameVerifier.java b/src/main/java/com/neovisionaries/ws/client/OkHostnameVerifier.java
new file mode 100644
index 0000000..64e9b1d
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/OkHostnameVerifier.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.neovisionaries.ws.client;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A HostnameVerifier consistent with RFC 2818.
+ */
+final class OkHostnameVerifier implements HostnameVerifier {
+ public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
+
+ /**
+ * Quick and dirty pattern to differentiate IP addresses from hostnames. This
+ * is an approximation of Android's private InetAddress#isNumeric API.
+ *
+ *
This matches IPv6 addresses as a hex string containing at least one
+ * colon, and possibly including dots after the first colon. It matches IPv4
+ * addresses as strings containing only decimal digits and dots. This pattern
+ * matches strings like "a:.23" and "54" that are neither IP addresses nor
+ * hostnames; they will be verified as IP addresses (which is a more strict
+ * verification).
+ */
+ private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
+
+ private static final int ALT_DNS_NAME = 2;
+ private static final int ALT_IPA_NAME = 7;
+
+ private OkHostnameVerifier() {
+ }
+
+ @Override
+ public boolean verify(String host, SSLSession session) {
+ try {
+ Certificate[] certificates = session.getPeerCertificates();
+ return verify(host, (X509Certificate) certificates[0]);
+ } catch (SSLException e) {
+ return false;
+ }
+ }
+
+ public boolean verify(String host, X509Certificate certificate) {
+ return verifyAsIpAddress(host) ? verifyIpAddress(host, certificate) : verifyHostName(host, certificate);
+ }
+
+ static boolean verifyAsIpAddress(String host) {
+ return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
+ }
+
+ /**
+ * Returns true if {@code certificate} matches {@code ipAddress}.
+ */
+ private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
+ List altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
+ if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if {@code certificate} matches {@code hostName}.
+ */
+ private boolean verifyHostName(String hostName, X509Certificate certificate) {
+ hostName = hostName.toLowerCase(Locale.US);
+ boolean hasDns = false;
+ List altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
+ hasDns = true;
+ if (verifyHostName(hostName, altNames.get(i))) {
+ return true;
+ }
+ }
+
+ if (!hasDns) {
+ X500Principal principal = certificate.getSubjectX500Principal();
+ // RFC 2818 advises using the most specific name for matching.
+ String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
+ if (cn != null) {
+ return verifyHostName(hostName, cn);
+ }
+ }
+
+ return false;
+ }
+
+ public static List allSubjectAltNames(X509Certificate certificate) {
+ List altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ List altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ List result = new ArrayList(altIpaNames.size() + altDnsNames.size());
+ result.addAll(altIpaNames);
+ result.addAll(altDnsNames);
+ return result;
+ }
+
+ private static List getSubjectAltNames(X509Certificate certificate, int type) {
+ List result = new ArrayList();
+ try {
+ Collection> subjectAltNames = certificate.getSubjectAlternativeNames();
+ if (subjectAltNames == null) {
+ return Collections.emptyList();
+ }
+ for (Object subjectAltName : subjectAltNames) {
+ List> entry = (List>) subjectAltName;
+ if (entry == null || entry.size() < 2) {
+ continue;
+ }
+ Integer altNameType = (Integer) entry.get(0);
+ if (altNameType == null) {
+ continue;
+ }
+ if (altNameType == type) {
+ String altName = (String) entry.get(1);
+ if (altName != null) {
+ result.add(altName);
+ }
+ }
+ }
+ return result;
+ } catch (CertificateParsingException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}.
+ *
+ * @param hostName lower-case host name.
+ * @param pattern domain name pattern from certificate. May be a wildcard pattern such as
+ * {@code *.android.com}.
+ */
+ private boolean verifyHostName(String hostName, String pattern) {
+ // Basic sanity checks
+ // Check length == 0 instead of .isEmpty() to support Java 5.
+ if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith(".")) || (hostName.endsWith(".."))) {
+ // Invalid domain name
+ return false;
+ }
+ if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith(".")) || (pattern.endsWith(".."))) {
+ // Invalid pattern/domain name
+ return false;
+ }
+
+ // Normalize hostName and pattern by turning them into absolute domain names if they are not
+ // yet absolute. This is needed because server certificates do not normally contain absolute
+ // names or patterns, but they should be treated as absolute. At the same time, any hostName
+ // presented to this method should also be treated as absolute for the purposes of matching
+ // to the server certificate.
+ // www.android.com matches www.android.com
+ // www.android.com matches www.android.com.
+ // www.android.com. matches www.android.com.
+ // www.android.com. matches www.android.com
+ if (!hostName.endsWith(".")) {
+ hostName += '.';
+ }
+ if (!pattern.endsWith(".")) {
+ pattern += '.';
+ }
+ // hostName and pattern are now absolute domain names.
+
+ pattern = pattern.toLowerCase(Locale.US);
+ // hostName and pattern are now in lower case -- domain names are case-insensitive.
+
+ if (!pattern.contains("*")) {
+ // Not a wildcard pattern -- hostName and pattern must match exactly.
+ return hostName.equals(pattern);
+ }
+ // Wildcard pattern
+
+ // WILDCARD PATTERN RULES:
+ // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
+ // only character in that label (i.e., must match the whole left-most label).
+ // For example, *.example.com is permitted, while *a.example.com, a*.example.com,
+ // a*b.example.com, a.*.example.com are not permitted.
+ // 2. Asterisk (*) cannot match across domain name labels.
+ // For example, *.example.com matches test.example.com but does not match
+ // sub.test.example.com.
+ // 3. Wildcard patterns for single-label domain names are not permitted.
+
+ if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
+ // Asterisk (*) is only permitted in the left-most domain name label and must be the only
+ // character in that label
+ return false;
+ }
+
+ // Optimization: check whether hostName is too short to match the pattern. hostName must be at
+ // least as long as the pattern because asterisk must match the whole left-most label and
+ // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters.
+ if (hostName.length() < pattern.length()) {
+ // hostName too short to match the pattern.
+ return false;
+ }
+
+ if ("*.".equals(pattern)) {
+ // Wildcard pattern for single-label domain name -- not permitted.
+ return false;
+ }
+
+ // hostName must end with the region of pattern following the asterisk.
+ String suffix = pattern.substring(1);
+ if (!hostName.endsWith(suffix)) {
+ // hostName does not end with the suffix
+ return false;
+ }
+
+ // Check that asterisk did not match across domain name labels.
+ int suffixStartIndexInHostName = hostName.length() - suffix.length();
+ if ((suffixStartIndexInHostName > 0) && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) {
+ // Asterisk is matching across domain name labels -- not permitted.
+ return false;
+ }
+
+ // hostName matches pattern
+ return true;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/OpeningHandshakeException.java b/src/main/java/com/neovisionaries/ws/client/OpeningHandshakeException.java
new file mode 100644
index 0000000..ff0a84c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/OpeningHandshakeException.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * An exception raised due to a violation against the WebSocket protocol.
+ *
+ * @since 1.19
+ */
+public class OpeningHandshakeException extends WebSocketException {
+ private static final long serialVersionUID = 1L;
+
+
+ private final StatusLine mStatusLine;
+ private final Map> mHeaders;
+ private final byte[] mBody;
+
+
+ OpeningHandshakeException(WebSocketError error, String message, StatusLine statusLine, Map> headers) {
+ this(error, message, statusLine, headers, null);
+ }
+
+
+ OpeningHandshakeException(WebSocketError error, String message, StatusLine statusLine, Map> headers, byte[] body) {
+ super(error, message);
+
+ mStatusLine = statusLine;
+ mHeaders = headers;
+ mBody = body;
+ }
+
+
+ /**
+ * Get the status line contained in the WebSocket opening handshake
+ * response from the server.
+ *
+ * @return
+ * The status line.
+ */
+ public StatusLine getStatusLine() {
+ return mStatusLine;
+ }
+
+
+ /**
+ * Get the HTTP headers contained in the WebSocket opening handshake
+ * response from the server.
+ *
+ * @return
+ * The HTTP headers. The returned map is an instance of
+ * {@link java.util.TreeMap TreeMap} with {@link
+ * String#CASE_INSENSITIVE_ORDER} comparator.
+ */
+ public Map> getHeaders() {
+ return mHeaders;
+ }
+
+
+ /**
+ * Get the response body contained in the WebSocket opening handshake
+ * response from the server.
+ *
+ *
+ * This method returns a non-null value only when (1) the status code
+ * is not 101 (Switching Protocols), (2) the response from the server
+ * has a response body, (3) the response has "Content-Length" header,
+ * and (4) no error occurred during reading the response body. In other
+ * cases, this method returns {@code null}.
+ *
+ *
+ * @return
+ * The response body.
+ */
+ public byte[] getBody() {
+ return mBody;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PayloadGenerator.java b/src/main/java/com/neovisionaries/ws/client/PayloadGenerator.java
new file mode 100644
index 0000000..77c3216
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PayloadGenerator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Payload generator.
+ *
+ * @since 1.20
+ *
+ * @author Takahiko Kawasaki
+ */
+public interface PayloadGenerator {
+ /**
+ * Generate a payload of a frame.
+ *
+ *
+ * Note that the maximum payload length of control frames
+ * (e.g. ping frames) is 125 in bytes. Therefore, the length
+ * of a byte array returned from this method must not exceed
+ * 125 bytes.
+ *
+ *
+ * @return
+ * A payload of a frame.
+ */
+ byte[] generate();
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PerMessageCompressionExtension.java b/src/main/java/com/neovisionaries/ws/client/PerMessageCompressionExtension.java
new file mode 100644
index 0000000..4827312
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PerMessageCompressionExtension.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Per-Message Compression Extension (RFC 7692).
+ *
+ * @see RFC 7692
+ */
+abstract class PerMessageCompressionExtension extends WebSocketExtension {
+ public PerMessageCompressionExtension(String name) {
+ super(name);
+ }
+
+
+ public PerMessageCompressionExtension(WebSocketExtension source) {
+ super(source);
+ }
+
+
+ /**
+ * Decompress the compressed message.
+ */
+ protected abstract byte[] decompress(byte[] compressed) throws WebSocketException;
+
+
+ /**
+ * Compress the plain message.
+ */
+ protected abstract byte[] compress(byte[] plain) throws WebSocketException;
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PerMessageDeflateExtension.java b/src/main/java/com/neovisionaries/ws/client/PerMessageDeflateExtension.java
new file mode 100644
index 0000000..6d9ac5e
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PerMessageDeflateExtension.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.Map;
+
+
+/**
+ * Per-Message Deflate Extension (7. The "permessage-deflate" Extension in
+ * RFC 7692).
+ *
+ * @see 7. The "permessage-deflate" Extension in RFC 7692
+ */
+class PerMessageDeflateExtension extends PerMessageCompressionExtension {
+ private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
+ private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
+ private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits";
+ private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits";
+ private static final byte[] COMPRESSION_TERMINATOR = {(byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF};
+
+ private static final int MIN_BITS = 8;
+ private static final int MAX_BITS = 15;
+ private static final int MIN_WINDOW_SIZE = 256;
+ private static final int MAX_WINDOW_SIZE = 32768;
+ private static final int INCOMING_SLIDING_WINDOW_MARGIN = 1024;
+
+ private boolean mServerNoContextTakeover;
+ private boolean mClientNoContextTakeover;
+ private int mServerWindowSize = MAX_WINDOW_SIZE;
+ private int mClientWindowSize = MAX_WINDOW_SIZE;
+ private int mIncomingSlidingWindowBufferSize;
+ private ByteArray mIncomingSlidingWindow;
+
+
+ public PerMessageDeflateExtension() {
+ super(WebSocketExtension.PERMESSAGE_DEFLATE);
+ }
+
+
+ public PerMessageDeflateExtension(String name) {
+ super(name);
+ }
+
+
+ @Override
+ void validate() throws WebSocketException {
+ // For each parameter
+ for (Map.Entry entry : getParameters().entrySet()) {
+ validateParameter(entry.getKey(), entry.getValue());
+ }
+
+ mIncomingSlidingWindowBufferSize = mServerWindowSize + INCOMING_SLIDING_WINDOW_MARGIN;
+ }
+
+
+ public boolean isServerNoContextTakeover() {
+ return mServerNoContextTakeover;
+ }
+
+
+ public boolean isClientNoContextTakeover() {
+ return mClientNoContextTakeover;
+ }
+
+
+ public int getServerWindowSize() {
+ return mServerWindowSize;
+ }
+
+
+ public int getClientWindowSize() {
+ return mClientWindowSize;
+ }
+
+
+ private void validateParameter(String key, String value) throws WebSocketException {
+ if (SERVER_NO_CONTEXT_TAKEOVER.equals(key)) {
+ mServerNoContextTakeover = true;
+ } else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(key)) {
+ mClientNoContextTakeover = true;
+ } else if (SERVER_MAX_WINDOW_BITS.equals(key)) {
+ mServerWindowSize = computeWindowSize(key, value);
+ } else if (CLIENT_MAX_WINDOW_BITS.equals(key)) {
+ mClientWindowSize = computeWindowSize(key, value);
+ } else {
+ // permessage-deflate extension contains an unsupported parameter.
+ throw new WebSocketException(WebSocketError.PERMESSAGE_DEFLATE_UNSUPPORTED_PARAMETER, "permessage-deflate extension contains an unsupported parameter: " + key);
+ }
+ }
+
+
+ private int computeWindowSize(String key, String value) throws WebSocketException {
+ int bits = extractMaxWindowBits(key, value);
+ int size = MIN_WINDOW_SIZE;
+
+ for (int i = MIN_BITS; i < bits; ++i) {
+ size *= 2;
+ }
+
+ return size;
+ }
+
+
+ private int extractMaxWindowBits(String key, String value) throws WebSocketException {
+ int bits = parseMaxWindowBits(value);
+
+ if (bits < 0) {
+ String message = String.format(
+ "The value of %s parameter of permessage-deflate extension is invalid: %s",
+ key, value);
+
+ throw new WebSocketException(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, message);
+ }
+
+ return bits;
+ }
+
+
+ private int parseMaxWindowBits(String value) {
+ if (value == null) {
+ return -1;
+ }
+
+ int bits;
+
+ try {
+ bits = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+
+ if (bits < MIN_BITS || MAX_BITS < bits) {
+ return -1;
+ }
+
+ return bits;
+ }
+
+
+ @Override
+ protected byte[] decompress(byte[] compressed) throws WebSocketException {
+ // Append 0x00, 0x00, 0xFF and 0xFF.
+ //
+ // From RFC 7692, 7.2.2. Decompression
+ //
+ // An endpoint uses the following algorithm to decompress a message.
+ //
+ // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of
+ // the payload of the message.
+ //
+ // 2. Decompress the resulting data using DEFLATE.
+ //
+ //
+ // From RFC 1979, 2.1. Packet Format, Data, The 3rd paragraph:
+ //
+ // The basic format of the compressed data is precisely described by
+ // the 'Deflate' Compressed Data Format Specification[3]. Each
+ // transmitted packet must begin at a 'deflate' block boundary, to
+ // ensure synchronization when incompressible data resets the
+ // transmitter's state; to ensure this, each transmitted packet must
+ // be terminated with a zero-length 'deflate' non-compressed block
+ // (BTYPE of 00). This means that the last four bytes of the
+ // compressed format must be 0x00 0x00 0xFF 0xFF. These bytes MUST
+ // be removed before transmission; the receiver can reinsert them if
+ // required by the implementation.
+ //
+ int inputLen = compressed.length + COMPRESSION_TERMINATOR.length;
+
+ // Wrap the compressed byte array with ByteArray.
+ ByteArray input = new ByteArray(inputLen);
+ input.put(compressed);
+ input.put(COMPRESSION_TERMINATOR);
+
+ if (mIncomingSlidingWindow == null) {
+ mIncomingSlidingWindow = new ByteArray(mIncomingSlidingWindowBufferSize);
+ }
+
+ // The size of the sliding window before decompression.
+ int outPos = mIncomingSlidingWindow.length();
+
+ try {
+ // Decompress.
+ DeflateDecompressor.decompress(input, mIncomingSlidingWindow);
+ } catch (Exception e) {
+ // Failed to decompress the message.
+ throw new WebSocketException(
+ WebSocketError.DECOMPRESSION_ERROR,
+ String.format("Failed to decompress the message: %s", e.getMessage()), e);
+ }
+
+ byte[] output = mIncomingSlidingWindow.toBytes(outPos);
+
+ // Shrink the size of the incoming sliding window.
+ mIncomingSlidingWindow.shrink(mIncomingSlidingWindowBufferSize);
+
+ if (mServerNoContextTakeover) {
+ // No need to remember the message for the next decompression.
+ mIncomingSlidingWindow.clear();
+ }
+
+ return output;
+ }
+
+
+ @Override
+ protected byte[] compress(byte[] plain) throws WebSocketException {
+ if (canCompress(plain) == false) {
+ // Compression should not be performed.
+ return plain;
+ }
+
+ // From RFC 7692, 7.2.1. Compression
+ //
+ // An endpoint uses the following algorithm to compress a message.
+ //
+ // 1. Compress all the octets of the payload of the message using
+ // DEFLATE.
+ //
+ // 2. If the resulting data does not end with an empty DEFLATE block
+ // with no compression (the "BTYPE" bits are set to 00), append an
+ // empty DEFLATE block with no compression to the tail end.
+ //
+ // 3. Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
+ // After this step, the last octet of the compressed data contains
+ // (possibly part of) the DEFLATE header bits with the "BTYPE" bits
+ // set to 00.
+
+ try {
+ // Compress.
+ byte[] compressed = DeflateCompressor.compress(plain);
+
+ // Adjust the compressed data to comply with RFC 7692.
+ return adjustCompressedData(compressed);
+ } catch (Exception e) {
+ // Failed to compress the message.
+ throw new WebSocketException(
+ WebSocketError.COMPRESSION_ERROR,
+ String.format("Failed to compress the message: %s", e.getMessage()), e);
+ }
+ }
+
+
+ private boolean canCompress(byte[] plain) {
+ // The current compression implementation (DeflateCompressor)
+ // cannot control the size of the internal sliding window on
+ // the client side.
+ //
+ // Therefore, compression should not be performed if there is
+ // a possibility that Huffman codes in compressed data may
+ // refer to bigger distances than the agreed sliding window
+ // size (which is computed based on client_max_window_bits).
+ //
+ // From RFC 7692, 7.2.1. Compression
+ //
+ // If the "agreed parameters" contain the "client_max_window_bits"
+ // extension parameter with a value of w, the client MUST NOT use
+ // an LZ77 sliding window longer than the w-th power of 2 bytes
+ // to compress messages to send.
+ //
+
+ // If the agreed sliding window size is the maximum value allowed
+ // by the DEFLATE specification, the size of the internal sliding
+ // window of the compressor does not have to be cared about.
+ if (mClientWindowSize == MAX_WINDOW_SIZE) {
+ // Can be compressed.
+ return true;
+ }
+
+ // Otherwise, considering the fact that the current implementation
+ // does not support context takeover on the client side, it can be
+ // said that Huffman codes in compressed data will not refer to
+ // bigger distances than the agreed sliding window size if the size
+ // of the original plain data is less than the agreed sliding window
+ // size.
+ if (plain.length < mClientWindowSize) {
+ // Can be compressed.
+ return true;
+ }
+
+ // Cannot exclude the possibility that Huffman codes in compressed
+ // data may refer to bigger distances than the agreed sliding window
+ // size. Therefore, compression should not be performed.
+ return false;
+ }
+
+
+ private static byte[] adjustCompressedData(byte[] compressed) throws FormatException {
+ // Wrap the compressed data with ByteArray. '+1' here is for 3 bits,
+ // '000', that may be appended at the bottom of this method.
+ ByteArray data = new ByteArray(compressed.length + 1);
+ data.put(compressed);
+
+ // The data is compressed on a bit basis, so use a bit index.
+ int[] bitIndex = new int[1];
+
+ // The flag to indicate whether the last block in the original
+ // compressed data is an empty block with no compression.
+ boolean[] hasEmptyBlock = new boolean[1];
+
+ // Skip all blocks one by one until the end.
+ // skipBlock() returns false if no more block exists.
+ while (skipBlock(data, bitIndex, hasEmptyBlock)) ;
+
+ // If the last block is an empty block with no compression.
+ if (hasEmptyBlock[0]) {
+ // In this case, it is enough to drop the last four bytes
+ // (0x00 0x00 0xFF 0xFF).
+ return data.toBytes(0, ((bitIndex[0] - 1) / 8) + 1 - 4);
+ }
+
+ // Append 3 bits, '000'.
+ //
+ // The first bit is BFINAL and its value is '0'. Note that '1'
+ // is not used here although the block being appended is the
+ // last block. It's because some server-side implementations
+ // fail to inflate compressed data with BFINAL=1.
+ //
+ // The second and the third bits are '00' and it means NO
+ // COMPRESSION.
+ appendEmptyBlock(data, bitIndex);
+
+ // Convert the ByteArray to byte[].
+ return data.toBytes(0, ((bitIndex[0] - 1) / 8) + 1);
+ }
+
+
+ private static void appendEmptyBlock(ByteArray data, int[] bitIndex) {
+ int shift = bitIndex[0] % 8;
+
+ // ? = used (0 or 1), x = unused (= 0).
+ //
+ // | Current | After 3 bits are appended
+ // ----------+----------+---------------------------
+ // shift = 1 | xxxxxxx? | xxxx000?
+ // shift = 2 | xxxxxx?? | xxx000??
+ // shift = 3 | xxxxx??? | xx000???
+ // shift = 4 | xxxx???? | x000????
+ // shift = 5 | xxx????? | 000?????
+ // shift = 6 | xx?????? | 00?????? xxxxxxx0
+ // shift = 7 | x??????? | 0??????? xxxxxx00
+ // shift = 0 | ???????? | ???????? xxxxx000
+
+ switch (shift) {
+ case 6:
+ case 7:
+ case 0:
+ data.put(0);
+ }
+
+ // Update the bit index for the 3 bits.
+ bitIndex[0] += 3;
+ }
+
+
+ private static boolean skipBlock(ByteArray input, int[] bitIndex, boolean[] hasEmptyBlock) throws FormatException {
+ // Each block has a block header which consists of 3 bits.
+ // See 3.2.3. of RFC 1951.
+
+ // The first bit indicates whether the block is the last one or not.
+ boolean last = input.readBit(bitIndex);
+
+ if (last) {
+ // Clear the BFINAL bit because some server-side implementations
+ // fail to inflate compressed data with BFINAL=1.
+ input.clearBit(bitIndex[0] - 1);
+ }
+
+ // The combination of the second and the third bits indicate the
+ // compression type of the block. Compression types are as follows:
+ //
+ // 00: No compression.
+ // 01: Compressed with fixed Huffman codes
+ // 10: Compressed with dynamic Huffman codes
+ // 11: Reserved (error)
+ //
+ int type = input.readBits(bitIndex, 2);
+
+ // This flag becomes true if skipPlainBlock() is called and it returns 0.
+ boolean plain0 = false;
+
+ switch (type) {
+ // No compression
+ case 0:
+ // Skip the plain block. skipPlainBlock() returns the data length.
+ plain0 = (skipPlainBlock(input, bitIndex) == 0);
+ break;
+
+ // Compressed with fixed Huffman codes
+ case 1:
+ skipFixedBlock(input, bitIndex);
+ break;
+
+ // Compressed with dynamic Huffman codes
+ case 2:
+ skipDynamicBlock(input, bitIndex);
+ break;
+
+ // Bad format
+ default:
+ // Bad compression type at the bit index.
+ String message = String.format(
+ "[%s] Bad compression type '11' at the bit index '%d'.",
+ PerMessageDeflateExtension.class.getSimpleName(), bitIndex[0]);
+
+ throw new FormatException(message);
+ }
+
+ // If no more data are available.
+ if (input.length() <= (bitIndex[0] / 8)) {
+ // Last even if the BFINAL bit is false.
+ last = true;
+ }
+
+ if (last && plain0) {
+ // The last block is an empty block with no compression.
+ hasEmptyBlock[0] = true;
+ }
+
+ // Return true if this block is not the last one.
+ return !last;
+ }
+
+
+ private static int skipPlainBlock(ByteArray input, int[] bitIndex) {
+ // 3.2.4 Non-compressed blocks (BTYPE=00)
+
+ // Skip any remaining bits in current partially processed byte.
+ int bi = (bitIndex[0] + 7) & ~7;
+
+ // Data copy is performed on a byte basis, so convert the bit index
+ // to a byte index.
+ int index = bi / 8;
+
+ // LEN: 2 bytes. The data length.
+ int len = (input.get(index) & 0xFF) + (input.get(index + 1) & 0xFF) * 256;
+
+ // NLEN: 2 bytes. The one's complement of LEN.
+
+ // Skip LEN and NLEN.
+ index += 4;
+
+ // Make the bitIndex point to the bit next to
+ // the end of the copied data.
+ bitIndex[0] = (index + len) * 8;
+
+ return len;
+ }
+
+
+ private static void skipFixedBlock(ByteArray input, int[] bitIndex) throws FormatException {
+ // 3.2.6 Compression with fixed Huffman codes (BTYPE=01)
+
+ // Inflate the compressed data using the pre-defined
+ // conversion tables. The specification says in 3.2.2
+ // as follows.
+ //
+ // The only differences between the two compressed
+ // cases is how the Huffman codes for the literal/
+ // length and distance alphabets are defined.
+ //
+ // The "two compressed cases" in the above sentence are
+ // "fixed Huffman codes" and "dynamic Huffman codes".
+ skipData(input, bitIndex,
+ FixedLiteralLengthHuffman.getInstance(),
+ FixedDistanceHuffman.getInstance());
+ }
+
+
+ private static void skipDynamicBlock(ByteArray input, int[] bitIndex) throws FormatException {
+ // 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
+
+ // Read 2 tables. One is a table to convert "code value of literal/length
+ // alphabet" into "literal/length symbol". The other is a table to convert
+ // "code value of distance alphabet" into "distance symbol".
+ Huffman[] tables = new Huffman[2];
+ DeflateUtil.readDynamicTables(input, bitIndex, tables);
+
+ skipData(input, bitIndex, tables[0], tables[1]);
+ }
+
+
+ private static void skipData(ByteArray input, int[] bitIndex, Huffman literalLengthHuffman, Huffman distanceHuffman) throws FormatException {
+ // 3.2.5 Compressed blocks (length and distance codes)
+
+ while (true) {
+ // Read a literal/length symbol from the input.
+ int literalLength = literalLengthHuffman.readSym(input, bitIndex);
+
+ // Symbol value '256' indicates the end.
+ if (literalLength == 256) {
+ // End of this data.
+ break;
+ }
+
+ // Symbol values from 0 to 255 represent literal values.
+ if (0 <= literalLength && literalLength <= 255) {
+ // Output as is.
+ continue;
+ }
+
+ // Symbol values from 257 to 285 represent pairs.
+ // Depending on symbol values, some extra bits in the input may be
+ // consumed to compute the length.
+ DeflateUtil.readLength(input, bitIndex, literalLength);
+
+ // Read the distance from the input.
+ DeflateUtil.readDistance(input, bitIndex, distanceHuffman);
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PeriodicalFrameSender.java b/src/main/java/com/neovisionaries/ws/client/PeriodicalFrameSender.java
new file mode 100644
index 0000000..c9fef25
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PeriodicalFrameSender.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+
+abstract class PeriodicalFrameSender {
+ private final WebSocket mWebSocket;
+ private final String mTimerName;
+ private Timer mTimer;
+ private boolean mScheduled;
+ private long mInterval;
+ private PayloadGenerator mGenerator;
+
+
+ public PeriodicalFrameSender(WebSocket webSocket, String timerName, PayloadGenerator generator) {
+ mWebSocket = webSocket;
+ mTimerName = timerName;
+ mGenerator = generator;
+ }
+
+
+ public void start() {
+ setInterval(getInterval());
+ }
+
+
+ public void stop() {
+ synchronized (this) {
+ if (mTimer == null) {
+ return;
+ }
+
+ mScheduled = false;
+ mTimer.cancel();
+ }
+ }
+
+
+ public long getInterval() {
+ synchronized (this) {
+ return mInterval;
+ }
+ }
+
+
+ public void setInterval(long interval) {
+ if (interval < 0) {
+ interval = 0;
+ }
+
+ synchronized (this) {
+ mInterval = interval;
+ }
+
+ if (interval == 0) {
+ return;
+ }
+
+ if (mWebSocket.isOpen() == false) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mTimer == null) {
+ mTimer = new Timer(mTimerName);
+ }
+
+ if (mScheduled == false) {
+ mScheduled = schedule(mTimer, new Task(), interval);
+ }
+ }
+ }
+
+
+ public PayloadGenerator getPayloadGenerator() {
+ synchronized (this) {
+ return mGenerator;
+ }
+ }
+
+
+ public void setPayloadGenerator(PayloadGenerator generator) {
+ synchronized (this) {
+ mGenerator = generator;
+ }
+ }
+
+
+ private final class Task extends TimerTask {
+ @Override
+ public void run() {
+ doTask();
+ }
+ }
+
+
+ private void doTask() {
+ synchronized (this) {
+ if (mInterval == 0 || mWebSocket.isOpen() == false) {
+ mScheduled = false;
+
+ // Not schedule a new task.
+ return;
+ }
+
+ // Create a frame and send it to the server.
+ mWebSocket.sendFrame(createFrame());
+
+ // Schedule a new task.
+ mScheduled = schedule(mTimer, new Task(), mInterval);
+ }
+ }
+
+
+ private WebSocketFrame createFrame() {
+ // Prepare payload of a frame.
+ byte[] payload = generatePayload();
+
+ // Let the subclass create a frame.
+ return createFrame(payload);
+ }
+
+
+ private byte[] generatePayload() {
+ if (mGenerator == null) {
+ return null;
+ }
+
+ try {
+ // Let the generator generate payload.
+ return mGenerator.generate();
+ } catch (Throwable t) {
+ // Empty payload.
+ return null;
+ }
+ }
+
+
+ private static boolean schedule(Timer timer, Task task, long interval) {
+ try {
+ // Schedule the task.
+ timer.schedule(task, interval);
+
+ // Successfully scheduled the task.
+ return true;
+ } catch (RuntimeException e) {
+ // Failed to schedule the task. Probably, the exception is
+ // an IllegalStateException which is raised due to one of
+ // the following reasons (according to the Javadoc):
+ //
+ // (1) if task was already scheduled or cancelled,
+ // (2) timer was cancelled, or
+ // (3) timer thread terminated.
+ //
+ // Because a new task is created every time this method is
+ // called and there is no code to call TimerTask.cancel(),
+ // (1) cannot be a reason.
+ //
+ // In either case of (2) and (3), we don't have to retry to
+ // schedule the task, because the timer that is expected to
+ // host the task will stop or has stopped anyway.
+ return false;
+ }
+ }
+
+
+ protected abstract WebSocketFrame createFrame(byte[] payload);
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PingSender.java b/src/main/java/com/neovisionaries/ws/client/PingSender.java
new file mode 100644
index 0000000..45a0b3f
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PingSender.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class PingSender extends PeriodicalFrameSender {
+ private static final String TIMER_NAME = "PingSender";
+
+
+ public PingSender(WebSocket webSocket, PayloadGenerator generator) {
+ super(webSocket, TIMER_NAME, generator);
+ }
+
+
+ @Override
+ protected WebSocketFrame createFrame(byte[] payload) {
+ return WebSocketFrame.createPingFrame(payload);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/PongSender.java b/src/main/java/com/neovisionaries/ws/client/PongSender.java
new file mode 100644
index 0000000..fc47f99
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/PongSender.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class PongSender extends PeriodicalFrameSender {
+ private static final String TIMER_NAME = "PongSender";
+
+
+ public PongSender(WebSocket webSocket, PayloadGenerator generator) {
+ super(webSocket, TIMER_NAME, generator);
+ }
+
+
+ @Override
+ protected WebSocketFrame createFrame(byte[] payload) {
+ return WebSocketFrame.createPongFrame(payload);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ProxyHandshaker.java b/src/main/java/com/neovisionaries/ws/client/ProxyHandshaker.java
new file mode 100644
index 0000000..571de37
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ProxyHandshaker.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.List;
+import java.util.Map;
+
+
+class ProxyHandshaker {
+ private static final String RN = "\r\n";
+ private final Socket mSocket;
+ private final String mHost;
+ private final int mPort;
+ private final ProxySettings mSettings;
+
+
+ public ProxyHandshaker(Socket socket, String host, int port, ProxySettings settings) {
+ mSocket = socket;
+ mHost = host;
+ mPort = port;
+ mSettings = settings;
+ }
+
+
+ public void perform() throws IOException {
+ // Send a CONNECT request to the proxy server.
+ sendRequest();
+
+ // Receive a response.
+ receiveResponse();
+ }
+
+
+ private void sendRequest() throws IOException {
+ // Build a CONNECT request.
+ String request = buildRequest();
+
+ // Convert the request to a byte array.
+ byte[] requestBytes = Misc.getBytesUTF8(request);
+
+ // Get the stream to send data to the proxy server.
+ OutputStream output = mSocket.getOutputStream();
+
+ // Send the request to the proxy server.
+ output.write(requestBytes);
+ output.flush();
+ }
+
+
+ private String buildRequest() {
+ String host = String.format("%s:%d", mHost, mPort);
+
+ // CONNECT
+ StringBuilder builder = new StringBuilder().append("CONNECT ").append(host).append(" HTTP/1.1").append(RN).append("Host: ").append(host).append(RN);
+
+
+ // Additional headers
+ addHeaders(builder);
+
+ // Proxy-Authorization
+ addProxyAuthorization(builder);
+
+ // The entire request.
+ return builder.append(RN).toString();
+ }
+
+
+ private void addHeaders(StringBuilder builder) {
+ // For each additional header.
+ for (Map.Entry> header : mSettings.getHeaders().entrySet()) {
+ // Header name.
+ String name = header.getKey();
+
+ // For each header value.
+ for (String value : header.getValue()) {
+ if (value == null) {
+ value = "";
+ }
+
+ builder.append(name).append(": ").append(value).append(RN);
+ }
+ }
+ }
+
+
+ private void addProxyAuthorization(StringBuilder builder) {
+ String id = mSettings.getId();
+
+ if (id == null || id.length() == 0) {
+ return;
+ }
+
+ String password = mSettings.getPassword();
+
+ if (password == null) {
+ password = "";
+ }
+
+ // {id}:{password}
+ String credentials = String.format("%s:%s", id, password);
+
+ // The current implementation always uses Basic Authentication.
+ builder.append("Proxy-Authorization: Basic ").append(Base64.encode(credentials)).append(RN);
+ }
+
+
+ private void receiveResponse() throws IOException {
+ // Get the stream to read data from the proxy server.
+ InputStream input = mSocket.getInputStream();
+
+ // Read the status line.
+ readStatusLine(input);
+
+ // Skip HTTP headers, including an empty line (= the separator
+ // between the header part and the body part).
+ skipHeaders(input);
+ }
+
+
+ private void readStatusLine(InputStream input) throws IOException {
+ // Read the status line.
+ String statusLine = Misc.readLine(input, "UTF-8");
+
+ // If the response from the proxy server does not contain a status line.
+ if (statusLine == null || statusLine.length() == 0) {
+ throw new IOException("The response from the proxy server does not contain a status line.");
+ }
+
+ // Expect "HTTP/1.1 200 Connection established"
+ String[] elements = statusLine.split(" +", 3);
+
+ if (elements.length < 2) {
+ throw new IOException("The status line in the response from the proxy server is badly formatted. " + "The status line is: " + statusLine);
+ }
+
+ // If the status code is not "200".
+ if ("200".equals(elements[1]) == false) {
+ throw new IOException("The status code in the response from the proxy server is not '200 Connection established'. " + "The status line is: " + statusLine);
+ }
+
+ // OK. A connection was established.
+ }
+
+
+ private void skipHeaders(InputStream input) throws IOException {
+ // The number of normal letters in a line.
+ int count = 0;
+
+ while (true) {
+ // Read a byte from the stream.
+ int ch = input.read();
+
+ // If the end of the stream was reached.
+ if (ch == -1) {
+ // Unexpected EOF.
+ throw new EOFException("The end of the stream from the proxy server was reached unexpectedly.");
+ }
+
+ // If the end of the line was reached.
+ if (ch == '\n') {
+ // If there is no normal byte in the line.
+ if (count == 0) {
+ // An empty line (the separator) was found.
+ return;
+ }
+
+ // Reset the counter and go to the next line.
+ count = 0;
+ continue;
+ }
+
+ // If the read byte is not a carriage return.
+ if (ch != '\r') {
+ // Increment the number of normal bytes on the line.
+ ++count;
+ continue;
+ }
+
+ // Read the next byte.
+ ch = input.read();
+
+ // If the end of the stream was reached.
+ if (ch == -1) {
+ // Unexpected EOF.
+ throw new EOFException("The end of the stream from the proxy server was reached unexpectedly after a carriage return.");
+ }
+
+ if (ch != '\n') {
+ // Regard the last '\r' as a normal byte as well as the current 'ch'.
+ count += 2;
+ continue;
+ }
+
+ // '\r\n' was detected.
+
+ // If there is no normal byte in the line.
+ if (count == 0) {
+ // An empty line (the separator) was found.
+ return;
+ }
+
+ // Reset the counter and go to the next line.
+ count = 0;
+ }
+ }
+
+
+ /**
+ * To be able to verify the hostname of the certificate received
+ * if a connection is made to an https/wss endpoint, access to this
+ * hostname is required.
+ *
+ * @return the hostname of the server the proxy is asked to connect to.
+ */
+ String getProxiedHostname() {
+ return mHost;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ProxySettings.java b/src/main/java/com/neovisionaries/ws/client/ProxySettings.java
new file mode 100644
index 0000000..09f902a
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ProxySettings.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+
+/**
+ * Proxy settings.
+ *
+ *
+ * If a proxy server's host name is set (= if {@link #getHost()}
+ * returns a non-null value), a socket factory that creates a
+ * socket to communicate with the proxy server is selected based
+ * on the settings of this {@code ProxySettings} instance. The
+ * following is the concrete flow to select a socket factory.
+ *
+ * If an {@link SSLContext} instance has been set by {@link
+ * #setSSLContext(SSLContext)}, the value returned from {@link
+ * SSLContext#getSocketFactory()} method of the instance is used.
+ *
+ * Otherwise, if an {@link SSLSocketFactory} instance has been
+ * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the
+ * instance is used.
+ *
+ * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()}
+ * is used.
+ *
+ * If a {@link SocketFactory} instance has been set by {@link
+ * #setSocketFactory(SocketFactory)}, the instance is used.
+ *
+ * Otherwise, the value returned from {@link SocketFactory#getDefault()}
+ * is used.
+ *
+ *
+ *
+ *
+ *
+ * Note that the current implementation supports only Basic Authentication
+ * for authentication at the proxy server.
+ *
+ *
+ * @see WebSocketFactory#getProxySettings()
+ *
+ * @since 1.3
+ */
+public class ProxySettings {
+ private final WebSocketFactory mWebSocketFactory;
+ private final Map> mHeaders;
+ private final SocketFactorySettings mSocketFactorySettings;
+ private boolean mSecure;
+ private String mHost;
+ private int mPort;
+ private String mId;
+ private String mPassword;
+
+
+ ProxySettings(WebSocketFactory factory) {
+ mWebSocketFactory = factory;
+ mHeaders = new TreeMap>(String.CASE_INSENSITIVE_ORDER);
+ mSocketFactorySettings = new SocketFactorySettings();
+
+ reset();
+ }
+
+
+ /**
+ * Get the associated {@link WebSocketFactory} instance.
+ */
+ public WebSocketFactory getWebSocketFactory() {
+ return mWebSocketFactory;
+ }
+
+
+ /**
+ * Reset the proxy settings. To be concrete, parameter values are
+ * set as shown below.
+ *
+ *
+ *
+ *
+ *
+ *
Name
+ *
Value
+ *
Description
+ *
+ *
+ *
+ *
+ *
Secure
+ *
{@code false}
+ *
Use TLS to connect to the proxy server or not.
+ *
+ *
+ *
Host
+ *
{@code null}
+ *
The host name of the proxy server.
+ *
+ *
+ *
Port
+ *
{@code -1}
+ *
The port number of the proxy server.
+ *
+ *
+ *
ID
+ *
{@code null}
+ *
The ID for authentication at the proxy server.
+ *
+ *
+ *
Password
+ *
{@code null}
+ *
The password for authentication at the proxy server.
+ *
+ *
+ *
Headers
+ *
Cleared
+ *
Additional HTTP headers passed to the proxy server.
+ *
+ *
+ *
+ *
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings reset() {
+ mSecure = false;
+ mHost = null;
+ mPort = -1;
+ mId = null;
+ mPassword = null;
+ mHeaders.clear();
+
+ return this;
+ }
+
+
+ /**
+ * Check whether use of TLS is enabled or disabled.
+ *
+ * @return
+ * {@code true} if TLS is used in the communication with
+ * the proxy server.
+ */
+ public boolean isSecure() {
+ return mSecure;
+ }
+
+
+ /**
+ * Enable or disable use of TLS.
+ *
+ * @param secure
+ * {@code true} to use TLS in the communication with
+ * the proxy server.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setSecure(boolean secure) {
+ mSecure = secure;
+
+ return this;
+ }
+
+
+ /**
+ * Get the host name of the proxy server.
+ *
+ *
+ * The default value is {@code null}. If this method returns
+ * a non-null value, it is used as the proxy server.
+ *
+ *
+ * @return
+ * The host name of the proxy server.
+ */
+ public String getHost() {
+ return mHost;
+ }
+
+
+ /**
+ * Set the host name of the proxy server.
+ *
+ *
+ * If a non-null value is set, it is used as the proxy server.
+ *
+ *
+ * @param host
+ * The host name of the proxy server.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setHost(String host) {
+ mHost = host;
+
+ return this;
+ }
+
+
+ /**
+ * Get the port number of the proxy server.
+ *
+ *
+ * The default value is {@code -1}. {@code -1} means that
+ * the default port number ({@code 80} for non-secure
+ * connections and {@code 443} for secure connections)
+ * should be used.
+ *
+ *
+ * @return
+ * The port number of the proxy server.
+ */
+ public int getPort() {
+ return mPort;
+ }
+
+
+ /**
+ * Set the port number of the proxy server.
+ *
+ *
+ * If {@code -1} is set, the default port number ({@code 80}
+ * for non-secure connections and {@code 443} for secure
+ * connections) is used.
+ *
+ *
+ * @param port
+ * The port number of the proxy server.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setPort(int port) {
+ mPort = port;
+
+ return this;
+ }
+
+
+ /**
+ * Get the ID for authentication at the proxy server.
+ *
+ *
+ * The default value is {@code null}. If this method returns
+ * a non-null value, it is used as the ID for authentication
+ * at the proxy server. To be concrete, the value is used to
+ * generate the value of Proxy-Authorization header.
+ *
+ *
+ * @return
+ * The ID for authentication at the proxy server.
+ */
+ public String getId() {
+ return mId;
+ }
+
+
+ /**
+ * Set the ID for authentication at the proxy server.
+ *
+ *
+ * If a non-null value is set, it is used as the ID for
+ * authentication at the proxy server. To be concrete, the
+ * value is used to generate the value of Proxy-Authorization header.
+ *
+ *
+ * @param id
+ * The ID for authentication at the proxy server.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setId(String id) {
+ mId = id;
+
+ return this;
+ }
+
+
+ /**
+ * Get the password for authentication at the proxy server.
+ *
+ * @return
+ * The password for authentication at the proxy server.
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+
+ /**
+ * Set the password for authentication at the proxy server.
+ *
+ * @param password
+ * The password for authentication at the proxy server.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setPassword(String password) {
+ mPassword = password;
+
+ return this;
+ }
+
+
+ /**
+ * Set credentials for authentication at the proxy server.
+ * This method is an alias of {@link #setId(String) setId}{@code
+ * (id).}{@link #setPassword(String) setPassword}{@code
+ * (password)}.
+ *
+ * @param id
+ * The ID.
+ *
+ * @param password
+ * The password.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setCredentials(String id, String password) {
+ return setId(id).setPassword(password);
+ }
+
+
+ /**
+ * Set the proxy server by a URI. See the description of
+ * {@link #setServer(URI)} about how the parameters are updated.
+ *
+ * @param uri
+ * The URI of the proxy server. If {@code null} is given,
+ * none of the parameters are updated.
+ *
+ * @return
+ * {@code this} object.
+ *
+ * @throws IllegalArgumentException
+ * Failed to convert the given string to a {@link URI} instance.
+ */
+ public ProxySettings setServer(String uri) {
+ if (uri == null) {
+ return this;
+ }
+
+ return setServer(URI.create(uri));
+ }
+
+
+ /**
+ * Set the proxy server by a URL. See the description of
+ * {@link #setServer(URI)} about how the parameters are updated.
+ *
+ * @param url
+ * The URL of the proxy server. If {@code null} is given,
+ * none of the parameters are updated.
+ *
+ * @return
+ * {@code this} object.
+ *
+ * @throws IllegalArgumentException
+ * Failed to convert the given URL to a {@link URI} instance.
+ */
+ public ProxySettings setServer(URL url) {
+ if (url == null) {
+ return this;
+ }
+
+ try {
+ return setServer(url.toURI());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+
+ /**
+ * Set the proxy server by a URI. The parameters are updated as
+ * described below.
+ *
+ *
+ *
+ *
Secure
+ *
+ * If the URI contains the scheme part and its value is
+ * either {@code "http"} or {@code "https"} (case-insensitive),
+ * the {@code secure} parameter is updated to {@code false}
+ * or to {@code true} accordingly. In other cases, the parameter
+ * is not updated.
+ *
+ *
ID & Password
+ *
+ * If the URI contains the userinfo part and the ID embedded
+ * in the userinfo part is not an empty string, the {@code
+ * id} parameter and the {@code password} parameter are updated
+ * accordingly. In other cases, the parameters are not updated.
+ *
+ *
Host
+ *
+ * The {@code host} parameter is always updated by the given URI.
+ *
+ *
Port
+ *
+ * The {@code port} parameter is always updated by the given URI.
+ *
+ *
+ *
+ *
+ * @param uri
+ * The URI of the proxy server. If {@code null} is given,
+ * none of the parameters is updated.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings setServer(URI uri) {
+ if (uri == null) {
+ return this;
+ }
+
+ String scheme = uri.getScheme();
+ String userInfo = uri.getUserInfo();
+ String host = uri.getHost();
+ int port = uri.getPort();
+
+ return setServer(scheme, userInfo, host, port);
+ }
+
+
+ private ProxySettings setServer(String scheme, String userInfo, String host, int port) {
+ setByScheme(scheme);
+ setByUserInfo(userInfo);
+ mHost = host;
+ mPort = port;
+
+ return this;
+ }
+
+
+ private void setByScheme(String scheme) {
+ if ("http".equalsIgnoreCase(scheme)) {
+ mSecure = false;
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ mSecure = true;
+ }
+ }
+
+
+ private void setByUserInfo(String userInfo) {
+ if (userInfo == null) {
+ return;
+ }
+
+ String[] pair = userInfo.split(":", 2);
+ String id;
+ String pw;
+
+ switch (pair.length) {
+ case 2:
+ id = pair[0];
+ pw = pair[1];
+ break;
+
+ case 1:
+ id = pair[0];
+ pw = null;
+ break;
+
+ default:
+ return;
+ }
+
+ if (id.length() == 0) {
+ return;
+ }
+
+ mId = id;
+ mPassword = pw;
+ }
+
+
+ /**
+ * Get additional HTTP headers passed to the proxy server.
+ *
+ * @return
+ * Additional HTTP headers passed to the proxy server.
+ * The comparator of the returned map is {@link
+ * String#CASE_INSENSITIVE_ORDER}.
+ */
+ public Map> getHeaders() {
+ return mHeaders;
+ }
+
+
+ /**
+ * Add an additional HTTP header passed to the proxy server.
+ *
+ * @param name
+ * The name of an HTTP header (case-insensitive).
+ * If {@code null} or an empty string is given,
+ * nothing is added.
+ *
+ * @param value
+ * The value of the HTTP header.
+ *
+ * @return
+ * {@code this} object.
+ */
+ public ProxySettings addHeader(String name, String value) {
+ if (name == null || name.length() == 0) {
+ return this;
+ }
+
+ List list = mHeaders.get(name);
+
+ if (list == null) {
+ list = new ArrayList();
+ mHeaders.put(name, list);
+ }
+
+ list.add(value);
+
+ return this;
+ }
+
+
+ /**
+ * Get the socket factory that has been set by {@link
+ * #setSocketFactory(SocketFactory)}.
+ *
+ * @return
+ * The socket factory.
+ */
+ public SocketFactory getSocketFactory() {
+ return mSocketFactorySettings.getSocketFactory();
+ }
+
+
+ /**
+ * Set a socket factory.
+ *
+ * @param factory
+ * A socket factory.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public ProxySettings setSocketFactory(SocketFactory factory) {
+ mSocketFactorySettings.setSocketFactory(factory);
+
+ return this;
+ }
+
+
+ /**
+ * Get the SSL socket factory that has been set by {@link
+ * #setSSLSocketFactory(SSLSocketFactory)}.
+ *
+ * @return
+ * The SSL socket factory.
+ */
+ public SSLSocketFactory getSSLSocketFactory() {
+ return mSocketFactorySettings.getSSLSocketFactory();
+ }
+
+
+ /**
+ * Set an SSL socket factory.
+ *
+ * @param factory
+ * An SSL socket factory.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public ProxySettings setSSLSocketFactory(SSLSocketFactory factory) {
+ mSocketFactorySettings.setSSLSocketFactory(factory);
+
+ return this;
+ }
+
+
+ /**
+ * Get the SSL context that has been set by {@link #setSSLContext(SSLContext)}.
+ *
+ * @return
+ * The SSL context.
+ */
+ public SSLContext getSSLContext() {
+ return mSocketFactorySettings.getSSLContext();
+ }
+
+
+ /**
+ * Set an SSL context to get a socket factory.
+ *
+ * @param context
+ * An SSL context.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public ProxySettings setSSLContext(SSLContext context) {
+ mSocketFactorySettings.setSSLContext(context);
+
+ return this;
+ }
+
+
+ SocketFactory selectSocketFactory() {
+ return mSocketFactorySettings.selectSocketFactory(mSecure);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ReadingThread.java b/src/main/java/com/neovisionaries/ws/client/ReadingThread.java
new file mode 100644
index 0000000..64dd58b
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ReadingThread.java
@@ -0,0 +1,1030 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import com.neovisionaries.ws.client.StateManager.CloseInitiator;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static com.neovisionaries.ws.client.WebSocketOpcode.BINARY;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CLOSE;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CONTINUATION;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PING;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PONG;
+import static com.neovisionaries.ws.client.WebSocketOpcode.TEXT;
+import static com.neovisionaries.ws.client.WebSocketState.CLOSED;
+import static com.neovisionaries.ws.client.WebSocketState.CLOSING;
+
+
+class ReadingThread extends WebSocketThread {
+ private boolean mStopRequested;
+ private WebSocketFrame mCloseFrame;
+ private List mContinuation = new ArrayList();
+ private final PerMessageCompressionExtension mPMCE;
+ private Object mCloseLock = new Object();
+ private Timer mCloseTimer;
+ private CloseTask mCloseTask;
+ private long mCloseDelay;
+ private boolean mNotWaitForCloseFrame;
+
+
+ public ReadingThread(WebSocket websocket) {
+ super("ReadingThread", websocket, ThreadType.READING_THREAD);
+
+ mPMCE = websocket.getPerMessageCompressionExtension();
+ }
+
+
+ @Override
+ public void runMain() {
+ try {
+ main();
+ } catch (Throwable t) {
+ // An uncaught throwable was detected in the reading thread.
+ WebSocketException cause = new WebSocketException(WebSocketError.UNEXPECTED_ERROR_IN_READING_THREAD, "An uncaught throwable was detected in the reading thread: " + t.getMessage(), t);
+
+ // Notify the listeners.
+ ListenerManager manager = mWebSocket.getListenerManager();
+ manager.callOnError(cause);
+ manager.callOnUnexpectedError(cause);
+ }
+
+ // Notify this reading thread finished.
+ notifyFinished();
+ }
+
+
+ private void main() {
+ mWebSocket.onReadingThreadStarted();
+
+ while (true) {
+ synchronized (this) {
+ if (mStopRequested) {
+ break;
+ }
+ }
+
+ // Receive a frame from the server.
+ WebSocketFrame frame = readFrame();
+
+ if (frame == null) {
+ // Something unexpected happened.
+ break;
+ }
+
+ // Handle the frame.
+ boolean keepReading = handleFrame(frame);
+
+ if (keepReading == false) {
+ break;
+ }
+ }
+
+ // Wait for a close frame if one has not been received yet.
+ waitForCloseFrame();
+
+ // Cancel a task which calls Socket.close() if running.
+ cancelClose();
+ }
+
+
+ void requestStop(long closeDelay) {
+ synchronized (this) {
+ if (mStopRequested) {
+ return;
+ }
+
+ mStopRequested = true;
+ }
+
+ // interrupt() may not interrupt a blocking socket read(), so calling
+ // interrupt() here may not work. interrupt() in Java is different
+ // from signal-based interruption in C which unblocks a read() system
+ // call. Anyway, let's mark this thread as interrupted.
+ interrupt();
+
+ // To surely unblock a read() call, Socket.close() needs to be called.
+ // Or, shutdownInterrupt() may work, but it is not explicitly stated
+ // in the JavaDoc. In either case, interruption should not be executed
+ // now because a close frame from the server should be waited for.
+ //
+ // So, let's schedule a task with some delay which calls Socket.close().
+ // However, in normal cases, a close frame will arrive from the server
+ // before the task calls Socket.close().
+ mCloseDelay = closeDelay;
+ scheduleClose();
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onFrame(WebSocket, WebSocketFrame) onFrame}
+ * method of the listeners.
+ */
+ private void callOnFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onContinuationFrame(WebSocket, WebSocketFrame)
+ * onContinuationFrame} method of the listeners.
+ */
+ private void callOnContinuationFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnContinuationFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onTextFrame(WebSocket, WebSocketFrame)
+ * onTextFrame} method of the listeners.
+ */
+ private void callOnTextFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnTextFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onBinaryFrame(WebSocket, WebSocketFrame)
+ * onBinaryFrame} method of the listeners.
+ */
+ private void callOnBinaryFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnBinaryFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onCloseFrame(WebSocket, WebSocketFrame)
+ * onCloseFrame} method of the listeners.
+ */
+ private void callOnCloseFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnCloseFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onPingFrame(WebSocket, WebSocketFrame)
+ * onPingFrame} method of the listeners.
+ */
+ private void callOnPingFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnPingFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onPongFrame(WebSocket, WebSocketFrame)
+ * onPongFrame} method of the listeners.
+ */
+ private void callOnPongFrame(WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnPongFrame(frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onTextMessage(WebSocket, String)
+ * onTextMessage} method of the listeners.
+ */
+ private void callOnTextMessage(byte[] data) {
+ try {
+ // Interpret the byte array as a string.
+ // OutOfMemoryError may happen when the size of data is too big.
+ String message = Misc.toStringUTF8(data);
+
+ // Call onTextMessage() method of the listeners.
+ callOnTextMessage(message);
+ } catch (Throwable t) {
+ // Failed to convert payload data into a string.
+ WebSocketException wse = new WebSocketException(WebSocketError.TEXT_MESSAGE_CONSTRUCTION_ERROR, "Failed to convert payload data into a string: " + t.getMessage(), t);
+
+ // Notify the listeners that text message construction failed.
+ callOnError(wse);
+ callOnTextMessageError(wse, data);
+ }
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onTextMessage(WebSocket, String)
+ * onTextMessage} method of the listeners.
+ */
+ private void callOnTextMessage(String message) {
+ mWebSocket.getListenerManager().callOnTextMessage(message);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onBinaryMessage(WebSocket, String)
+ * onBinaryMessage} method of the listeners.
+ */
+ private void callOnBinaryMessage(byte[] message) {
+ mWebSocket.getListenerManager().callOnBinaryMessage(message);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onError(WebSocket, WebSocketException)
+ * onError} method of the listeners.
+ */
+ private void callOnError(WebSocketException cause) {
+ mWebSocket.getListenerManager().callOnError(cause);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onFrameError(WebSocket,
+ * WebSocketException, WebSocketFrame) onFrameError} method of the listeners.
+ */
+ private void callOnFrameError(WebSocketException cause, WebSocketFrame frame) {
+ mWebSocket.getListenerManager().callOnFrameError(cause, frame);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onMessageError(WebSocket, WebSocketException, List)
+ * onMessageError} method of the listeners.
+ */
+ private void callOnMessageError(WebSocketException cause, List frames) {
+ mWebSocket.getListenerManager().callOnMessageError(cause, frames);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onMessageDecompressionError(WebSocket, WebSocketException, byte[])
+ * onMessageDecompressionError} method of the listeners.
+ */
+ private void callOnMessageDecompressionError(WebSocketException cause, byte[] compressed) {
+ mWebSocket.getListenerManager().callOnMessageDecompressionError(cause, compressed);
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onTextMessageError(WebSocket, WebSocketException, byte[])
+ * onTextMessageError} method of the listeners.
+ */
+ private void callOnTextMessageError(WebSocketException cause, byte[] data) {
+ mWebSocket.getListenerManager().callOnTextMessageError(cause, data);
+ }
+
+
+ private WebSocketFrame readFrame() {
+ WebSocketFrame frame = null;
+ WebSocketException wse = null;
+
+ try {
+ // Receive a frame from the server.
+ frame = mWebSocket.getInput().readFrame();
+
+ // Verify the frame. If invalid, WebSocketException is thrown.
+ verifyFrame(frame);
+
+ // Return the verified frame.
+ return frame;
+ } catch (InterruptedIOException e) {
+ if (mStopRequested) {
+ // Thread.interrupt() interrupted a blocking socket read operation.
+ // This thread has been interrupted intentionally.
+ return null;
+ } else {
+ // Interruption occurred while a frame was being read from the web socket.
+ wse = new WebSocketException(WebSocketError.INTERRUPTED_IN_READING, "Interruption occurred while a frame was being read from the web socket: " + e.getMessage(), e);
+ }
+ } catch (IOException e) {
+ if (mStopRequested && isInterrupted()) {
+ // Socket.close() interrupted a blocking socket read operation.
+ // This thread has been interrupted intentionally.
+ return null;
+ } else {
+ // An I/O error occurred while a frame was being read from the web socket.
+ wse = new WebSocketException(WebSocketError.IO_ERROR_IN_READING, "An I/O error occurred while a frame was being read from the web socket: " + e.getMessage(), e);
+ }
+ } catch (WebSocketException e) {
+ // A protocol error.
+ wse = e;
+ }
+
+ boolean error = true;
+
+ // If the input stream of the WebSocket connection has reached the end
+ // without receiving a close frame from the server.
+ if (wse instanceof NoMoreFrameException) {
+ // Not wait for a close frame in waitForCloseFrame() which will be called later.
+ mNotWaitForCloseFrame = true;
+
+ // If the configuration of the WebSocket instance allows the behavior.
+ if (mWebSocket.isMissingCloseFrameAllowed()) {
+ error = false;
+ }
+ }
+
+ if (error) {
+ // Notify the listeners that an error occurred while a frame was being read.
+ callOnError(wse);
+ callOnFrameError(wse, frame);
+ }
+
+ // Create a close frame.
+ WebSocketFrame closeFrame = createCloseFrame(wse);
+
+ // Send the close frame.
+ mWebSocket.sendFrame(closeFrame);
+
+ // No WebSocket frame is available.
+ return null;
+ }
+
+
+ private void verifyFrame(WebSocketFrame frame) throws WebSocketException {
+ // Verify RSV1, RSV2 and RSV3.
+ verifyReservedBits(frame);
+
+ // The opcode of the frame must be known.
+ verifyFrameOpcode(frame);
+
+ // Frames from the server must not be masked.
+ verifyFrameMask(frame);
+
+ // Verify fragmentation conditions.
+ verifyFrameFragmentation(frame);
+
+ // Verify the size of the payload.
+ verifyFrameSize(frame);
+ }
+
+
+ private void verifyReservedBits(WebSocketFrame frame) throws WebSocketException {
+ // If extended use of web socket frames is allowed.
+ if (mWebSocket.isExtended()) {
+ // Do not check RSV1/RSV2/RSV3 bits.
+ return;
+ }
+
+ // RSV1, RSV2, RSV3
+ //
+ // The specification requires that these bits "be 0 unless an extension
+ // is negotiated that defines meanings for non-zero values".
+
+ verifyReservedBit1(frame);
+ verifyReservedBit2(frame);
+ verifyReservedBit3(frame);
+ }
+
+
+ /**
+ * Verify the RSV1 bit of a frame.
+ */
+ private void verifyReservedBit1(WebSocketFrame frame) throws WebSocketException {
+ // If a per-message compression extension has been agreed.
+ if (mPMCE != null) {
+ // Verify the RSV1 bit using the rule described in RFC 7692.
+ boolean verified = verifyReservedBit1ForPMCE(frame);
+
+ if (verified) {
+ return;
+ }
+ }
+
+ if (frame.getRsv1() == false) {
+ // No problem.
+ return;
+ }
+
+ // The RSV1 bit of a frame is set unexpectedly.
+ throw new WebSocketException(WebSocketError.UNEXPECTED_RESERVED_BIT, "The RSV1 bit of a frame is set unexpectedly.");
+ }
+
+
+ /**
+ * Verify the RSV1 bit of a frame using the rule described in RFC 7692.
+ * See 6. Framing
+ * in RFC 7692 for details.
+ */
+ private boolean verifyReservedBit1ForPMCE(WebSocketFrame frame) throws WebSocketException {
+ if (frame.isTextFrame() || frame.isBinaryFrame()) {
+ // The RSV1 of the first frame of a message is called
+ // "Per-Message Compressed" bit. It can be either 0 or 1.
+ // In other words, any value is okay.
+ return true;
+ }
+
+ // Further checking is required.
+ return false;
+ }
+
+
+ /**
+ * Verify the RSV2 bit of a frame.
+ */
+ private void verifyReservedBit2(WebSocketFrame frame) throws WebSocketException {
+ if (frame.getRsv2() == false) {
+ // No problem.
+ return;
+ }
+
+ // The RSV2 bit of a frame is set unexpectedly.
+ throw new WebSocketException(WebSocketError.UNEXPECTED_RESERVED_BIT, "The RSV2 bit of a frame is set unexpectedly.");
+ }
+
+
+ /**
+ * Verify the RSV3 bit of a frame.
+ */
+ private void verifyReservedBit3(WebSocketFrame frame) throws WebSocketException {
+ if (frame.getRsv3() == false) {
+ // No problem.
+ return;
+ }
+
+ // The RSV3 bit of a frame is set unexpectedly.
+ throw new WebSocketException(WebSocketError.UNEXPECTED_RESERVED_BIT, "The RSV3 bit of a frame is set unexpectedly.");
+ }
+
+
+ /**
+ * Ensure that the opcode of the give frame is a known one.
+ *
+ *
+ *
From RFC 6455, 5.2. Base Framing Protocol
+ *
+ * If an unknown opcode is received, the receiving endpoint MUST
+ * Fail the WebSocket Connection.
+ *
+ *
+ */
+ private void verifyFrameOpcode(WebSocketFrame frame) throws WebSocketException {
+ switch (frame.getOpcode()) {
+ case CONTINUATION:
+ case TEXT:
+ case BINARY:
+ case CLOSE:
+ case PING:
+ case PONG:
+ // Known opcode
+ return;
+
+ default:
+ break;
+ }
+
+ // If extended use of web socket frames is allowed.
+ if (mWebSocket.isExtended()) {
+ // Allow the unknown opcode.
+ return;
+ }
+
+ // A frame has an unknown opcode.
+ throw new WebSocketException(WebSocketError.UNKNOWN_OPCODE, "A frame has an unknown opcode: 0x" + Integer.toHexString(frame.getOpcode()));
+ }
+
+
+ /**
+ * Ensure that the given frame is not masked.
+ *
+ *
+ *
From RFC 6455, 5.1. Overview:
+ *
+ * A server MUST NOT mask any frames that it sends to the client.
+ * A client MUST close a connection if it detects a masked frame.
+ *
+ *
+ */
+ private void verifyFrameMask(WebSocketFrame frame) throws WebSocketException {
+ // If the frame is masked.
+ if (frame.getMask()) {
+ // A frame from the server is masked.
+ throw new WebSocketException(WebSocketError.FRAME_MASKED, "A frame from the server is masked.");
+ }
+ }
+
+
+ private void verifyFrameFragmentation(WebSocketFrame frame) throws WebSocketException {
+ // Control frames (see Section 5.5) MAY be injected in the
+ // middle of a fragmented message. Control frames themselves
+ // MUST NOT be fragmented.
+ if (frame.isControlFrame()) {
+ // If fragmented.
+ if (frame.getFin() == false) {
+ // A control frame is fragmented.
+ throw new WebSocketException(WebSocketError.FRAGMENTED_CONTROL_FRAME, "A control frame is fragmented.");
+ }
+
+ // No more requirements on a control frame.
+ return;
+ }
+
+ // True if a continuation has already started.
+ boolean continuationExists = (mContinuation.size() != 0);
+
+ // If the frame is a continuation frame.
+ if (frame.isContinuationFrame()) {
+ // There must already exist a continuation sequence.
+ if (continuationExists == false) {
+ // A continuation frame was detected although a continuation had not started.
+ throw new WebSocketException(WebSocketError.UNEXPECTED_CONTINUATION_FRAME, "A continuation frame was detected although a continuation had not started.");
+ }
+
+ // No more requirements on a continuation frame.
+ return;
+ }
+
+ // A data frame.
+
+ if (continuationExists) {
+ // A non-control frame was detected although the existing continuation had not been closed.
+ throw new WebSocketException(WebSocketError.CONTINUATION_NOT_CLOSED, "A non-control frame was detected although the existing continuation had not been closed.");
+ }
+ }
+
+
+ private void verifyFrameSize(WebSocketFrame frame) throws WebSocketException {
+ // If the frame is not a control frame.
+ if (frame.isControlFrame() == false) {
+ // Nothing to check.
+ return;
+ }
+
+ // RFC 6455, 5.5. Control Frames.
+ //
+ // All control frames MUST have a payload length of 125 bytes or less
+ // and MUST NOT be fragmented.
+ //
+
+ byte[] payload = frame.getPayload();
+
+ if (payload == null) {
+ // The frame does not have payload.
+ return;
+ }
+
+ if (125 < payload.length) {
+ // The payload size of a control frame exceeds the maximum size (125 bytes).
+ throw new WebSocketException(WebSocketError.TOO_LONG_CONTROL_FRAME_PAYLOAD, "The payload size of a control frame exceeds the maximum size (125 bytes): " + payload.length);
+ }
+ }
+
+
+ private WebSocketFrame createCloseFrame(WebSocketException wse) {
+ int closeCode;
+
+ switch (wse.getError()) {
+ // In WebSocketInputStream.readFrame()
+
+ case INSUFFICENT_DATA:
+ case INVALID_PAYLOAD_LENGTH:
+ case NO_MORE_FRAME:
+ closeCode = WebSocketCloseCode.UNCONFORMED;
+ break;
+
+ case TOO_LONG_PAYLOAD:
+ case INSUFFICIENT_MEMORY_FOR_PAYLOAD:
+ closeCode = WebSocketCloseCode.OVERSIZE;
+ break;
+
+ // In this.verifyFrame(WebSocketFrame)
+
+ case NON_ZERO_RESERVED_BITS:
+ case UNEXPECTED_RESERVED_BIT:
+ case UNKNOWN_OPCODE:
+ case FRAME_MASKED:
+ case FRAGMENTED_CONTROL_FRAME:
+ case UNEXPECTED_CONTINUATION_FRAME:
+ case CONTINUATION_NOT_CLOSED:
+ case TOO_LONG_CONTROL_FRAME_PAYLOAD:
+ closeCode = WebSocketCloseCode.UNCONFORMED;
+ break;
+
+ // In this.readFrame()
+
+ case INTERRUPTED_IN_READING:
+ case IO_ERROR_IN_READING:
+ closeCode = WebSocketCloseCode.VIOLATED;
+ break;
+
+ // Others (unexpected)
+
+ default:
+ closeCode = WebSocketCloseCode.VIOLATED;
+ break;
+ }
+
+ return WebSocketFrame.createCloseFrame(closeCode, wse.getMessage());
+ }
+
+
+ private boolean handleFrame(WebSocketFrame frame) {
+ // Notify the listeners that a frame was received.
+ callOnFrame(frame);
+
+ // Dispatch based on the opcode.
+ switch (frame.getOpcode()) {
+ case CONTINUATION:
+ return handleContinuationFrame(frame);
+
+ case TEXT:
+ return handleTextFrame(frame);
+
+ case BINARY:
+ return handleBinaryFrame(frame);
+
+ case CLOSE:
+ return handleCloseFrame(frame);
+
+ case PING:
+ return handlePingFrame(frame);
+
+ case PONG:
+ return handlePongFrame(frame);
+
+ default:
+ // Ignore the frame whose opcode is unknown. Keep reading.
+ return true;
+ }
+ }
+
+
+ private boolean handleContinuationFrame(WebSocketFrame frame) {
+ // Notify the listeners that a continuation frame was received.
+ callOnContinuationFrame(frame);
+
+ // Append the continuation frame to the existing continuation sequence.
+ mContinuation.add(frame);
+
+ // If the frame is not the last one for the continuation.
+ if (frame.getFin() == false) {
+ // Keep reading.
+ return true;
+ }
+
+ // Concatenate payloads of the frames. Decompression is performed
+ // when necessary.
+ byte[] data = getMessage(mContinuation);
+
+ // If the concatenation failed.
+ if (data == null) {
+ // Stop reading.
+ return false;
+ }
+
+ // If the continuation forms a text message.
+ if (mContinuation.get(0).isTextFrame()) {
+ // Notify the listeners that a text message was received.
+ callOnTextMessage(data);
+ } else {
+ // Notify the listeners that a binary message was received.
+ callOnBinaryMessage(data);
+ }
+
+ // Clear the continuation.
+ mContinuation.clear();
+
+ // Keep reading.
+ return true;
+ }
+
+
+ private byte[] getMessage(List frames) {
+ // Concatenate payloads of the frames.
+ byte[] data = concatenatePayloads(mContinuation);
+
+ // If the concatenation failed.
+ if (data == null) {
+ // Stop reading.
+ return null;
+ }
+
+ // If a per-message compression extension is enabled and
+ // the Per-Message Compressed bit of the first frame is set.
+ if (mPMCE != null && frames.get(0).getRsv1()) {
+ // Decompress the data.
+ data = decompress(data);
+ }
+
+ return data;
+ }
+
+
+ private byte[] concatenatePayloads(List frames) {
+ Throwable cause;
+
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ // For each web socket frame.
+ for (WebSocketFrame frame : frames) {
+ // Get the payload of the frame.
+ byte[] payload = frame.getPayload();
+
+ // If the payload is null or empty.
+ if (payload == null || payload.length == 0) {
+ continue;
+ }
+
+ // Append the payload.
+ baos.write(payload);
+ }
+
+ // Return the concatenated byte array.
+ return baos.toByteArray();
+ } catch (IOException e) {
+ cause = e;
+ } catch (OutOfMemoryError e) {
+ cause = e;
+ }
+
+ // Create a WebSocketException which has a cause.
+ WebSocketException wse = new WebSocketException(WebSocketError.MESSAGE_CONSTRUCTION_ERROR, "Failed to concatenate payloads of multiple frames to construct a message: " + cause.getMessage(), cause);
+
+ // Notify the listeners that message construction failed.
+ callOnError(wse);
+ callOnMessageError(wse, frames);
+
+ // Create a close frame with a close code of 1009 which
+ // indicates that the message is too big to process.
+ WebSocketFrame frame = WebSocketFrame.createCloseFrame(WebSocketCloseCode.OVERSIZE, wse.getMessage());
+
+ // Send the close frame.
+ mWebSocket.sendFrame(frame);
+
+ // Failed to construct a message.
+ return null;
+ }
+
+
+ private byte[] getMessage(WebSocketFrame frame) {
+ // The raw payload of the frame.
+ byte[] payload = frame.getPayload();
+
+ // If a per-message compression extension is enabled and
+ // the Per-Message Compressed bit of the frame is set.
+ if (mPMCE != null && frame.getRsv1()) {
+ // Decompress the payload.
+ payload = decompress(payload);
+ }
+
+ return payload;
+ }
+
+
+ private byte[] decompress(byte[] input) {
+ WebSocketException wse;
+
+ try {
+ // Decompress the message.
+ return mPMCE.decompress(input);
+ } catch (WebSocketException e) {
+ wse = e;
+ }
+
+ // Notify the listeners that decompression failed.
+ callOnError(wse);
+ callOnMessageDecompressionError(wse, input);
+
+ // Create a close frame with a close code of 1003 which
+ // indicates that the message cannot be accepted.
+ WebSocketFrame frame = WebSocketFrame.createCloseFrame(WebSocketCloseCode.UNACCEPTABLE, wse.getMessage());
+
+ // Send the close frame.
+ mWebSocket.sendFrame(frame);
+
+ // Failed to construct a message.
+ return null;
+ }
+
+
+ private boolean handleTextFrame(WebSocketFrame frame) {
+ // Notify the listeners that a text frame was received.
+ callOnTextFrame(frame);
+
+ // If the frame indicates the start of fragmentation.
+ if (frame.getFin() == false) {
+ // Start a continuation sequence.
+ mContinuation.add(frame);
+
+ // Keep reading.
+ return true;
+ }
+
+ // Get the payload of the frame. Decompression is performed
+ // when necessary.
+ byte[] payload = getMessage(frame);
+
+ // Notify the listeners that a text message was received.
+ callOnTextMessage(payload);
+
+ // Keep reading.
+ return true;
+ }
+
+
+ private boolean handleBinaryFrame(WebSocketFrame frame) {
+ // Notify the listeners that a binary frame was received.
+ callOnBinaryFrame(frame);
+
+ // If the frame indicates the start of fragmentation.
+ if (frame.getFin() == false) {
+ // Start a continuation sequence.
+ mContinuation.add(frame);
+
+ // Keep reading.
+ return true;
+ }
+
+ // Get the payload of the frame. Decompression is performed
+ // when necessary.
+ byte[] payload = getMessage(frame);
+
+ // Notify the listeners that a binary message was received.
+ callOnBinaryMessage(payload);
+
+ // Keep reading.
+ return true;
+ }
+
+
+ private boolean handleCloseFrame(WebSocketFrame frame) {
+ // Get the manager which manages the state of the web socket.
+ StateManager manager = mWebSocket.getStateManager();
+
+ // The close frame sent from the server.
+ mCloseFrame = frame;
+
+ boolean stateChanged = false;
+
+ synchronized (manager) {
+ // The current state of the web socket.
+ WebSocketState state = manager.getState();
+
+ // If the current state is neither CLOSING nor CLOSED.
+ if (state != CLOSING && state != CLOSED) {
+ // Change the state to CLOSING.
+ manager.changeToClosing(CloseInitiator.SERVER);
+
+ // This web socket has not sent a close frame yet,
+ // so schedule sending a close frame.
+
+ // RFC 6455, 5.5.1. Close
+ //
+ // When sending a Close frame in response, the endpoint
+ // typically echos the status code it received.
+ //
+
+ // Simply reuse the frame.
+ mWebSocket.sendFrame(frame);
+
+ stateChanged = true;
+ }
+ }
+
+ if (stateChanged) {
+ // Notify the listeners of the state change.
+ mWebSocket.getListenerManager().callOnStateChanged(CLOSING);
+ }
+
+ // Notify the listeners that a close frame was received.
+ callOnCloseFrame(frame);
+
+ // Stop reading.
+ return false;
+ }
+
+
+ private boolean handlePingFrame(WebSocketFrame frame) {
+ // Notify the listeners that a ping frame was received.
+ callOnPingFrame(frame);
+
+ // RFC 6455, 5.5.3. Pong
+ //
+ // A Pong frame sent in response to a Ping frame must
+ // have identical "Application data" as found in the
+ // message body of the Ping frame being replied to.
+
+ // Create a pong frame which has the same payload as
+ // the ping frame.
+ WebSocketFrame pong = WebSocketFrame.createPongFrame(frame.getPayload());
+
+ // Send the pong frame to the server.
+ mWebSocket.sendFrame(pong);
+
+ // Keep reading.
+ return true;
+ }
+
+
+ private boolean handlePongFrame(WebSocketFrame frame) {
+ // Notify the listeners that a pong frame was received.
+ callOnPongFrame(frame);
+
+ // Keep reading.
+ return true;
+ }
+
+
+ private void waitForCloseFrame() {
+ if (mNotWaitForCloseFrame) {
+ return;
+ }
+
+ // If a close frame has already been received.
+ if (mCloseFrame != null) {
+ return;
+ }
+
+ WebSocketFrame frame = null;
+
+ // Schedule a task which calls Socket.close() to prevent
+ // the code below from looping forever.
+ scheduleClose();
+
+ while (true) {
+ try {
+ // Read a frame from the server.
+ frame = mWebSocket.getInput().readFrame();
+ } catch (Throwable t) {
+ // Give up receiving a close frame.
+ break;
+ }
+
+ // If it is a close frame.
+ if (frame.isCloseFrame()) {
+ // Received a close frame. Finished.
+ mCloseFrame = frame;
+ break;
+ }
+
+ if (isInterrupted()) {
+ break;
+ }
+ }
+ }
+
+
+ private void notifyFinished() {
+ mWebSocket.onReadingThreadFinished(mCloseFrame);
+ }
+
+
+ private void scheduleClose() {
+ synchronized (mCloseLock) {
+ cancelCloseTask();
+ scheduleCloseTask();
+ }
+ }
+
+
+ private void scheduleCloseTask() {
+ mCloseTask = new CloseTask();
+ mCloseTimer = new Timer("ReadingThreadCloseTimer");
+ mCloseTimer.schedule(mCloseTask, mCloseDelay);
+ }
+
+
+ private void cancelClose() {
+ synchronized (mCloseLock) {
+ cancelCloseTask();
+ }
+ }
+
+
+ private void cancelCloseTask() {
+ if (mCloseTimer != null) {
+ mCloseTimer.cancel();
+ mCloseTimer = null;
+ }
+
+ if (mCloseTask != null) {
+ mCloseTask.cancel();
+ mCloseTask = null;
+ }
+ }
+
+
+ private class CloseTask extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ Socket socket = mWebSocket.getSocket();
+ socket.close();
+ } catch (Throwable t) {
+ // Ignore.
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/SocketConnector.java b/src/main/java/com/neovisionaries/ws/client/SocketConnector.java
new file mode 100644
index 0000000..de35b88
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/SocketConnector.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.IOException;
+import java.net.Socket;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+
+/**
+ * A class to connect to the server.
+ *
+ * @since 1.20
+ *
+ * @author Takahiko Kawasaki
+ */
+class SocketConnector {
+ private Socket mSocket;
+ private final Address mAddress;
+ private final int mConnectionTimeout;
+ private final ProxyHandshaker mProxyHandshaker;
+ private final SSLSocketFactory mSSLSocketFactory;
+ private final String mHost;
+ private final int mPort;
+
+
+ SocketConnector(Socket socket, Address address, int timeout) {
+ this(socket, address, timeout, null, null, null, 0);
+ }
+
+
+ SocketConnector(Socket socket, Address address, int timeout, ProxyHandshaker handshaker, SSLSocketFactory sslSocketFactory, String host, int port) {
+ mSocket = socket;
+ mAddress = address;
+ mConnectionTimeout = timeout;
+ mProxyHandshaker = handshaker;
+ mSSLSocketFactory = sslSocketFactory;
+ mHost = host;
+ mPort = port;
+ }
+
+
+ public Socket getSocket() {
+ return mSocket;
+ }
+
+
+ public int getConnectionTimeout() {
+ return mConnectionTimeout;
+ }
+
+
+ public void connect() throws WebSocketException {
+ try {
+ // Connect to the server (either a proxy or a WebSocket endpoint).
+ doConnect();
+ } catch (WebSocketException e) {
+ // Failed to connect the server.
+
+ try {
+ // Close the socket.
+ mSocket.close();
+ } catch (IOException ioe) {
+ // Ignore any error raised by close().
+ }
+
+ throw e;
+ }
+ }
+
+
+ private void doConnect() throws WebSocketException {
+ // True if a proxy server is set.
+ boolean proxied = mProxyHandshaker != null;
+
+ try {
+ // Connect to the server (either a proxy or a WebSocket endpoint).
+ mSocket.connect(mAddress.toInetSocketAddress(), mConnectionTimeout);
+
+ if (mSocket instanceof SSLSocket) {
+ // Verify that the hostname matches the certificate here since
+ // this is not automatically done by the SSLSocket.
+ verifyHostname((SSLSocket) mSocket, mAddress.getHostname());
+ }
+ } catch (IOException e) {
+ // Failed to connect the server.
+ String message = String.format("Failed to connect to %s'%s': %s", (proxied ? "the proxy " : ""), mAddress, e.getMessage());
+
+ // Raise an exception with SOCKET_CONNECT_ERROR.
+ throw new WebSocketException(WebSocketError.SOCKET_CONNECT_ERROR, message, e);
+ }
+
+ // If a proxy server is set.
+ if (proxied) {
+ // Perform handshake with the proxy server.
+ // SSL handshake is performed as necessary, too.
+ handshake();
+ }
+ }
+
+
+ private void verifyHostname(SSLSocket socket, String hostname) throws HostnameUnverifiedException {
+ // Hostname verifier.
+ OkHostnameVerifier verifier = OkHostnameVerifier.INSTANCE;
+
+ // The SSL session.
+ SSLSession session = socket.getSession();
+
+ // Verify the hostname.
+ if (verifier.verify(hostname, session)) {
+ // Verified. No problem.
+ return;
+ }
+
+ // The certificate of the peer does not match the expected hostname.
+ throw new HostnameUnverifiedException(socket, hostname);
+ }
+
+
+ /**
+ * Perform proxy handshake and optionally SSL handshake.
+ */
+ private void handshake() throws WebSocketException {
+ try {
+ // Perform handshake with the proxy server.
+ mProxyHandshaker.perform();
+ } catch (IOException e) {
+ // Handshake with the proxy server failed.
+ String message = String.format("Handshake with the proxy server (%s) failed: %s", mAddress, e.getMessage());
+
+ // Raise an exception with PROXY_HANDSHAKE_ERROR.
+ throw new WebSocketException(WebSocketError.PROXY_HANDSHAKE_ERROR, message, e);
+ }
+
+ if (mSSLSocketFactory == null) {
+ // SSL handshake with the WebSocket endpoint is not needed.
+ return;
+ }
+
+ try {
+ // Overlay the existing socket.
+ mSocket = mSSLSocketFactory.createSocket(mSocket, mHost, mPort, true);
+ } catch (IOException e) {
+ // Failed to overlay an existing socket.
+ String message = "Failed to overlay an existing socket: " + e.getMessage();
+
+ // Raise an exception with SOCKET_OVERLAY_ERROR.
+ throw new WebSocketException(WebSocketError.SOCKET_OVERLAY_ERROR, message, e);
+ }
+
+ try {
+ // Start the SSL handshake manually. As for the reason, see
+ // http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/samples/sockets/client/SSLSocketClient.java
+ ((SSLSocket) mSocket).startHandshake();
+
+ if (mSocket instanceof SSLSocket) {
+ // Verify that the proxied hostname matches the certificate here since
+ // this is not automatically done by the SSLSocket.
+ verifyHostname((SSLSocket) mSocket, mProxyHandshaker.getProxiedHostname());
+ }
+ } catch (IOException e) {
+ // SSL handshake with the WebSocket endpoint failed.
+ String message = String.format("SSL handshake with the WebSocket endpoint (%s) failed: %s", mAddress, e.getMessage());
+
+ // Raise an exception with SSL_HANDSHAKE_ERROR.
+ throw new WebSocketException(WebSocketError.SSL_HANDSHAKE_ERROR, message, e);
+ }
+ }
+
+
+ void closeSilently() {
+ try {
+ mSocket.close();
+ } catch (Throwable t) {
+ // Ignored.
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/SocketFactorySettings.java b/src/main/java/com/neovisionaries/ws/client/SocketFactorySettings.java
new file mode 100644
index 0000000..cf2ccbc
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/SocketFactorySettings.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+
+class SocketFactorySettings {
+ private SocketFactory mSocketFactory;
+ private SSLSocketFactory mSSLSocketFactory;
+ private SSLContext mSSLContext;
+
+
+ public SocketFactory getSocketFactory() {
+ return mSocketFactory;
+ }
+
+
+ public void setSocketFactory(SocketFactory factory) {
+ mSocketFactory = factory;
+ }
+
+
+ public SSLSocketFactory getSSLSocketFactory() {
+ return mSSLSocketFactory;
+ }
+
+
+ public void setSSLSocketFactory(SSLSocketFactory factory) {
+ mSSLSocketFactory = factory;
+ }
+
+
+ public SSLContext getSSLContext() {
+ return mSSLContext;
+ }
+
+
+ public void setSSLContext(SSLContext context) {
+ mSSLContext = context;
+ }
+
+
+ public SocketFactory selectSocketFactory(boolean secure) {
+ if (secure) {
+ if (mSSLContext != null) {
+ return mSSLContext.getSocketFactory();
+ }
+
+ if (mSSLSocketFactory != null) {
+ return mSSLSocketFactory;
+ }
+
+ return SSLSocketFactory.getDefault();
+ }
+
+ if (mSocketFactory != null) {
+ return mSocketFactory;
+ }
+
+ return SocketFactory.getDefault();
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/StateManager.java b/src/main/java/com/neovisionaries/ws/client/StateManager.java
new file mode 100644
index 0000000..d83d55e
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/StateManager.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import static com.neovisionaries.ws.client.WebSocketState.CLOSING;
+import static com.neovisionaries.ws.client.WebSocketState.CREATED;
+
+
+class StateManager {
+ enum CloseInitiator {
+ NONE,
+ SERVER,
+ CLIENT
+ }
+
+
+ private WebSocketState mState;
+ private CloseInitiator mCloseInitiator = CloseInitiator.NONE;
+
+
+ public StateManager() {
+ mState = CREATED;
+ }
+
+
+ public WebSocketState getState() {
+ return mState;
+ }
+
+
+ public void setState(WebSocketState state) {
+ mState = state;
+ }
+
+
+ public void changeToClosing(CloseInitiator closeInitiator) {
+ mState = CLOSING;
+
+ // Set the close initiator only when it has not been set yet.
+ if (mCloseInitiator == CloseInitiator.NONE) {
+ mCloseInitiator = closeInitiator;
+ }
+ }
+
+
+ public boolean getClosedByServer() {
+ return mCloseInitiator == CloseInitiator.SERVER;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/StatusLine.java b/src/main/java/com/neovisionaries/ws/client/StatusLine.java
new file mode 100644
index 0000000..6d1c9f5
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/StatusLine.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * HTTP status line returned from an HTTP server.
+ *
+ * @since 1.19
+ */
+public class StatusLine {
+ /**
+ * HTTP version.
+ */
+ private final String mHttpVersion;
+
+
+ /**
+ * Status code.
+ */
+ private final int mStatusCode;
+
+
+ /**
+ * Reason phrase.
+ */
+ private final String mReasonPhrase;
+
+
+ /**
+ * String representation of this instance (= the raw status line).
+ */
+ private final String mString;
+
+
+ /**
+ * Constructor with a raw status line.
+ *
+ * @param line
+ * A status line.
+ *
+ * @throws NullPointerException
+ * {@code line} is {@code null}
+ *
+ * @throws IllegalArgumentException
+ * The number of elements in {@code line} is less than 2.
+ *
+ * @throws NumberFormatException
+ * Failed to parse the second element in {@code line}
+ * as an integer.
+ */
+ StatusLine(String line) {
+ // HTTP-Version Status-Code Reason-Phrase
+ String[] elements = line.split(" +", 3);
+
+ if (elements.length < 2) {
+ throw new IllegalArgumentException();
+ }
+
+ mHttpVersion = elements[0];
+ mStatusCode = Integer.parseInt(elements[1]);
+ mReasonPhrase = (elements.length == 3) ? elements[2] : null;
+ mString = line;
+ }
+
+
+ /**
+ * Get the HTTP version.
+ *
+ * @return
+ * The HTTP version. For example, {@code "HTTP/1.1"}.
+ */
+ public String getHttpVersion() {
+ return mHttpVersion;
+ }
+
+
+ /**
+ * Get the status code.
+ *
+ * @return
+ * The status code. For example, {@code 404}.
+ */
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+
+ /**
+ * Get the reason phrase.
+ *
+ * @return
+ * The reason phrase. For example, {@code "Not Found"}.
+ */
+ public String getReasonPhrase() {
+ return mReasonPhrase;
+ }
+
+
+ /**
+ * Get the string representation of this instance, which is
+ * equal to the raw status line.
+ *
+ * @return
+ * The raw status line. For example,
+ * {@code "HTTP/1.1 404 Not Found"}.
+ */
+ @Override
+ public String toString() {
+ return mString;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/ThreadType.java b/src/main/java/com/neovisionaries/ws/client/ThreadType.java
new file mode 100644
index 0000000..df04f64
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/ThreadType.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Types of threads which are created internally in the implementation.
+ *
+ * @since 2.0
+ */
+public enum ThreadType {
+ /**
+ * A thread which reads WebSocket frames from the server
+ * (ReadingThread).
+ */
+ READING_THREAD,
+
+
+ /**
+ * A thread which sends WebSocket frames to the server
+ * (WritingThread).
+ */
+ WRITING_THREAD,
+
+
+ /**
+ * A thread which calls {@link WebSocket#connect()} asynchronously
+ * (ConnectThread).
+ */
+ CONNECT_THREAD,
+
+
+ /**
+ * A thread which does finalization of a {@link WebSocket} instance.
+ * (FinishThread).
+ */
+ FINISH_THREAD,
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/Token.java b/src/main/java/com/neovisionaries/ws/client/Token.java
new file mode 100644
index 0000000..36bb69d
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/Token.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+class Token {
+ /**
+ * Check if the given string conforms to the rules described
+ * in "2.2 Basic Rules" of RFC 2616.
+ */
+ public static boolean isValid(String token) {
+ if (token == null || token.length() == 0) {
+ return false;
+ }
+
+ int len = token.length();
+
+ for (int i = 0; i < len; ++i) {
+ if (isSeparator(token.charAt(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ public static boolean isSeparator(char ch) {
+ switch (ch) {
+ case '(':
+ case ')':
+ case '<':
+ case '>':
+ case '@':
+ case ',':
+ case ';':
+ case ':':
+ case '\\':
+ case '"':
+ case '/':
+ case '[':
+ case ']':
+ case '?':
+ case '=':
+ case '{':
+ case '}':
+ case ' ':
+ case '\t':
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+
+ public static String unquote(String text) {
+ if (text == null) {
+ return null;
+ }
+
+ int len = text.length();
+
+ if (len < 2 || text.charAt(0) != '"' || text.charAt(len - 1) != '"') {
+ return text;
+ }
+
+ text = text.substring(1, len - 1);
+
+ return unescape(text);
+ }
+
+
+ public static String unescape(String text) {
+ if (text == null) {
+ return null;
+ }
+
+ if (text.indexOf('\\') < 0) {
+ return text;
+ }
+
+ int len = text.length();
+ boolean escaped = false;
+ StringBuilder builder = new StringBuilder();
+
+
+ for (int i = 0; i < len; ++i) {
+ char ch = text.charAt(i);
+
+ if (ch == '\\' && escaped == false) {
+ escaped = true;
+ continue;
+ }
+
+ escaped = false;
+ builder.append(ch);
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocket.java b/src/main/java/com/neovisionaries/ws/client/WebSocket.java
new file mode 100644
index 0000000..d921ea7
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocket.java
@@ -0,0 +1,3169 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import com.neovisionaries.ws.client.StateManager.CloseInitiator;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+
+import static com.neovisionaries.ws.client.WebSocketState.CLOSED;
+import static com.neovisionaries.ws.client.WebSocketState.CLOSING;
+import static com.neovisionaries.ws.client.WebSocketState.CONNECTING;
+import static com.neovisionaries.ws.client.WebSocketState.CREATED;
+import static com.neovisionaries.ws.client.WebSocketState.OPEN;
+
+
+/**
+ * WebSocket.
+ *
+ *
Create WebSocketFactory
+ *
+ *
+ * {@link WebSocketFactory} is a factory class that creates
+ * {@link WebSocket} instances. The first step is to create a
+ * {@code WebSocketFactory} instance.
+ *
+ *
+ *
+ *
// Create a WebSocketFactory instance.
+ * WebSocketFactory factory = new {@link WebSocketFactory#WebSocketFactory()
+ * WebSocketFactory()};
+ *
+ *
+ *
+ * By default, {@code WebSocketFactory} uses {@link
+ * javax.net.SocketFactory SocketFactory}{@code .}{@link
+ * javax.net.SocketFactory#getDefault() getDefault()} for
+ * non-secure WebSocket connections ({@code ws:}) and {@link
+ * javax.net.ssl.SSLSocketFactory SSLSocketFactory}{@code
+ * .}{@link javax.net.ssl.SSLSocketFactory#getDefault()
+ * getDefault()} for secure WebSocket connections ({@code
+ * wss:}). You can change this default behavior by using
+ * {@code WebSocketFactory.}{@link
+ * WebSocketFactory#setSocketFactory(javax.net.SocketFactory)
+ * setSocketFactory} method, {@code WebSocketFactory.}{@link
+ * WebSocketFactory#setSSLSocketFactory(javax.net.ssl.SSLSocketFactory)
+ * setSSLSocketFactory} method and {@code WebSocketFactory.}{@link
+ * WebSocketFactory#setSSLContext(javax.net.ssl.SSLContext)
+ * setSSLContext} method. Note that you don't have to call a {@code
+ * setSSL*} method at all if you use the default SSL configuration.
+ * Also note that calling {@code setSSLSocketFactory} method has no
+ * meaning if you have called {@code setSSLContext} method. See the
+ * description of {@code WebSocketFactory.}{@link
+ * WebSocketFactory#createSocket(URI) createSocket(URI)} method for
+ * details.
+ *
+ *
+ *
+ * The following is an example to set a custom SSL context to a
+ * {@code WebSocketFactory} instance. (Again, you don't have to call a
+ * {@code setSSL*} method if you use the default SSL configuration.)
+ *
+ *
+ *
+ *
// Create a custom SSL context.
+ * SSLContext context = NaiveSSLContext.getInstance("TLS");
+ *
+ * // Set the custom SSL context.
+ * factory.{@link WebSocketFactory#setSSLContext(javax.net.ssl.SSLContext)
+ * setSSLContext}(context);
+ *
+ *
+ *
+ * NaiveSSLContext used in the above example is a factory class to
+ * create an {@link javax.net.ssl.SSLContext SSLContext} which naively
+ * accepts all certificates without verification. It's enough for testing
+ * purposes. When you see an error message
+ * "unable to find valid certificate path to requested target" while
+ * testing, try {@code NaiveSSLContext}.
+ *
+ *
+ *
HTTP Proxy
+ *
+ *
+ * If a WebSocket endpoint needs to be accessed via an HTTP proxy,
+ * information about the proxy server has to be set to a {@code
+ * WebSocketFactory} instance before creating a {@code WebSocket}
+ * instance. Proxy settings are represented by {@link ProxySettings}
+ * class. A {@code WebSocketFactory} instance has an associated
+ * {@code ProxySettings} instance and it can be obtained by calling
+ * {@code WebSocketFactory.}{@link WebSocketFactory#getProxySettings()
+ * getProxySettings()} method.
+ *
+ *
+ *
+ *
// Get the associated ProxySettings instance.
+ * {@link ProxySettings} settings = factory.{@link
+ * WebSocketFactory#getProxySettings() getProxySettings()};
+ *
+ *
+ *
+ * {@code ProxySettings} class has methods to set information about
+ * a proxy server such as {@link ProxySettings#setHost(String) setHost}
+ * method and {@link ProxySettings#setPort(int) setPort} method. The
+ * following is an example to set a secure (https) proxy
+ * server.
+ *
+ *
+ *
+ *
// Set a proxy server.
+ * settings.{@link ProxySettings#setServer(String)
+ * setServer}("https://proxy.example.com");
+ *
+ *
+ *
+ * If credentials are required for authentication at a proxy server,
+ * {@link ProxySettings#setId(String) setId} method and {@link
+ * ProxySettings#setPassword(String) setPassword} method, or
+ * {@link ProxySettings#setCredentials(String, String) setCredentials}
+ * method can be used to set the credentials. Note that, however,
+ * the current implementation supports only Basic Authentication.
+ *
+ *
+ *
+ *
// Set credentials for authentication at a proxy server.
+ * settings.{@link ProxySettings#setCredentials(String, String)
+ * setCredentials}(id, password);
+ *
+ *
+ *
+ *
Create WebSocket
+ *
+ *
+ * {@link WebSocket} class represents a WebSocket. Its instances are
+ * created by calling one of {@code createSocket} methods of a {@link
+ * WebSocketFactory} instance. Below is the simplest example to create
+ * a {@code WebSocket} instance.
+ *
+ *
+ *
+ *
// Create a WebSocket. The scheme part can be one of the following:
+ * // 'ws', 'wss', 'http' and 'https' (case-insensitive). The user info
+ * // part, if any, is interpreted as expected. If a raw socket failed
+ * // to be created, an IOException is thrown.
+ * WebSocket ws = new {@link WebSocketFactory#WebSocketFactory()
+ * WebSocketFactory()}
+ * .{@link WebSocketFactory#createSocket(String)
+ * createWebSocket}("ws://localhost/endpoint");
+ *
+ *
+ *
+ * There are two ways to set a timeout value for socket connection. The
+ * first way is to call {@link WebSocketFactory#setConnectionTimeout(int)
+ * setConnectionTimeout(int timeout)} method of {@code WebSocketFactory}.
+ *
+ *
+ *
+ *
// Create a WebSocket factory and set 5000 milliseconds as a timeout
+ * // value for socket connection.
+ * WebSocketFactory factory = new WebSocketFactory().{@link
+ * WebSocketFactory#setConnectionTimeout(int) setConnectionTimeout}(5000);
+ *
+ * // Create a WebSocket. The timeout value set above is used.
+ * WebSocket ws = factory.{@link WebSocketFactory#createSocket(String)
+ * createWebSocket}("ws://localhost/endpoint");
+ *
+ *
+ *
+ * The other way is to give a timeout value to a {@code createSocket} method.
+ *
+ *
+ *
+ *
// Create a WebSocket factory. The timeout value remains 0.
+ * WebSocketFactory factory = new WebSocketFactory();
+ *
+ * // Create a WebSocket with a socket connection timeout value.
+ * WebSocket ws = factory.{@link WebSocketFactory#createSocket(String, int)
+ * createWebSocket}("ws://localhost/endpoint", 5000);
+ *
+ *
+ *
+ * The timeout value is passed to {@link Socket#connect(java.net.SocketAddress, int)
+ * connect}{@code (}{@link java.net.SocketAddress SocketAddress}{@code , int)}
+ * method of {@link java.net.Socket}.
+ *
+ *
+ *
Register Listener
+ *
+ *
+ * After creating a {@code WebSocket} instance, you should call {@link
+ * #addListener(WebSocketListener)} method to register a {@link
+ * WebSocketListener} that receives WebSocket events. {@link
+ * WebSocketAdapter} is an empty implementation of {@link
+ * WebSocketListener} interface.
+ *
+ *
+ *
+ *
// Register a listener to receive WebSocket events.
+ * ws.{@link #addListener(WebSocketListener) addListener}(new {@link
+ * WebSocketAdapter#WebSocketAdapter() WebSocketAdapter()} {
+ * {@code @}Override
+ * public void {@link WebSocketListener#onTextMessage(WebSocket, String)
+ * onTextMessage}(WebSocket websocket, String message) throws Exception {
+ * // Received a text message.
+ * ......
+ * }
+ * });
+ *
+ *
+ *
+ * The table below is the list of callback methods defined in {@code WebSocketListener}
+ * interface.
+ *
+ * Before starting a WebSocket opening handshake with the server, you can configure the
+ * {@code WebSocket} instance by using the following methods.
+ *
Set whether to allow the server to close the connection without sending a close frame.
+ *
+ *
+ *
+ *
+ *
+ *
Connect To Server
+ *
+ *
+ * By calling {@link #connect()} method, connection to the server is
+ * established and a WebSocket opening handshake is performed
+ * synchronously. If an error occurred during the handshake,
+ * a {@link WebSocketException} would be thrown. Instead, when the
+ * handshake succeeds, the {@code connect()} implementation creates
+ * threads and starts them to read and write WebSocket frames
+ * asynchronously.
+ *
+ *
+ *
+ *
try
+ * {
+ * // Connect to the server and perform an opening handshake.
+ * // This method blocks until the opening handshake is finished.
+ * ws.{@link #connect()};
+ * }
+ * catch ({@link OpeningHandshakeException} e)
+ * {
+ * // A violation against the WebSocket protocol was detected
+ * // during the opening handshake.
+ * }
+ * catch ({@link HostnameUnverifiedException} e)
+ * {
+ * // The certificate of the peer does not match the expected hostname.
+ * }
+ * catch ({@link WebSocketException} e)
+ * {
+ * // Failed to establish a WebSocket connection.
+ * }
+ *
+ *
+ *
+ * In some cases, {@code connect()} method throws {@link OpeningHandshakeException}
+ * which is a subclass of {@code WebSocketException} (since version 1.19).
+ * {@code OpeningHandshakeException} provides additional methods such as
+ * {@link OpeningHandshakeException#getStatusLine() getStatusLine()},
+ * {@link OpeningHandshakeException#getHeaders() getHeaders()} and
+ * {@link OpeningHandshakeException#getBody() getBody()} to access the
+ * response from a server. The following snippet is an example to print
+ * information that the exception holds.
+ *
+ * Also, {@code connect()} method throws {@link HostnameUnverifiedException}
+ * which is a subclass of {@code WebSocketException} (since version 2.1) when
+ * the certificate of the peer does not match the expected hostname.
+ *
+ *
+ *
Connect To Server Asynchronously
+ *
+ *
+ * The simplest way to call {@code connect()} method asynchronously is to
+ * use {@link #connectAsynchronously()} method. The implementation of the
+ * method creates a thread and calls {@code connect()} method in the thread.
+ * When the {@code connect()} call failed, {@link
+ * WebSocketListener#onConnectError(WebSocket, WebSocketException)
+ * onConnectError()} of {@code WebSocketListener} would be called. Note that
+ * {@code onConnectError()} is called only when {@code connectAsynchronously()}
+ * was used and the {@code connect()} call executed in the background thread
+ * failed. Neither direct synchronous {@code connect()} nor
+ * {@link WebSocket#connect(java.util.concurrent.ExecutorService)
+ * connect(ExecutorService)} (described below) will trigger the callback method.
+ *
+ *
+ *
+ *
// Connect to the server asynchronously.
+ * ws.{@link #connectAsynchronously()};
+ *
+ *
+ *
+ *
+ * Another way to call {@code connect()} method asynchronously is to use
+ * {@link #connect(ExecutorService)} method. The method performs a WebSocket
+ * opening handshake asynchronously using the given {@link ExecutorService}.
+ *
+ *
+ *
+ *
// Prepare an ExecutorService.
+ * {@link ExecutorService} es = {@link java.util.concurrent.Executors Executors}.{@link
+ * java.util.concurrent.Executors#newSingleThreadExecutor() newSingleThreadExecutor()};
+ *
+ * // Connect to the server asynchronously.
+ * {@link Future}{@code } future = ws.{@link #connect(ExecutorService) connect}(es);
+ *
+ * try
+ * {
+ * // Wait for the opening handshake to complete.
+ * future.get();
+ * }
+ * catch ({@link java.util.concurrent.ExecutionException ExecutionException} e)
+ * {
+ * if (e.getCause() instanceof {@link WebSocketException})
+ * {
+ * ......
+ * }
+ * }
+ *
+ *
+ *
+ * The implementation of {@code connect(ExecutorService)} method creates
+ * a {@link java.util.concurrent.Callable Callable}{@code }
+ * instance by calling {@link #connectable()} method and passes the
+ * instance to {@link ExecutorService#submit(Callable) submit(Callable)}
+ * method of the given {@code ExecutorService}. What the implementation
+ * of {@link Callable#call() call()} method of the {@code Callable}
+ * instance does is just to call the synchronous {@code connect()}.
+ *
+ *
+ *
Send Frames
+ *
+ *
+ * WebSocket frames can be sent by {@link #sendFrame(WebSocketFrame)}
+ * method. Other sendXxx methods such as {@link
+ * #sendText(String)} are aliases of {@code sendFrame} method. All of
+ * the sendXxx methods work asynchronously.
+ * However, under some conditions, sendXxx methods
+ * may block. See Congestion Control
+ * for details.
+ *
+ *
+ *
+ * Below
+ * are some examples of sendXxx methods. Note that
+ * in normal cases, you don't have to call {@link #sendClose()} method
+ * and {@link #sendPong()} (or their variants) explicitly because they
+ * are called automatically when appropriate.
+ *
+ *
+ *
+ *
// Send a text frame.
+ * ws.{@link #sendText(String) sendText}("Hello.");
+ *
+ * // Send a binary frame.
+ * byte[] binary = ......;
+ * ws.{@link #sendBinary(byte[]) sendBinary}(binary);
+ *
+ * // Send a ping frame.
+ * ws.{@link #sendPing(String) sendPing}("Are you there?");
+ *
+ *
+ *
+ * If you want to send fragmented frames, you have to know the details
+ * of the specification (5.4. Fragmentation). Below is an example to send a text message
+ * ({@code "How are you?"}) which consists of 3 fragmented frames.
+ *
+ *
+ *
+ *
// The first frame must be either a text frame or a binary frame.
+ * // And its FIN bit must be cleared.
+ * WebSocketFrame firstFrame = WebSocketFrame
+ * .{@link WebSocketFrame#createTextFrame(String)
+ * createTextFrame}("How ")
+ * .{@link WebSocketFrame#setFin(boolean) setFin}(false);
+ *
+ * // Subsequent frames must be continuation frames. The FIN bit of
+ * // all continuation frames except the last one must be cleared.
+ * // Note that the FIN bit of frames returned from
+ * // WebSocketFrame.createContinuationFrame methods is cleared, so
+ * // the example below does not clear the FIN bit explicitly.
+ * WebSocketFrame secondFrame = WebSocketFrame
+ * .{@link WebSocketFrame#createContinuationFrame(String)
+ * createContinuationFrame}("are ");
+ *
+ * // The last frame must be a continuation frame with the FIN bit set.
+ * // Note that the FIN bit of frames returned from
+ * // WebSocketFrame.createContinuationFrame methods is cleared, so
+ * // the FIN bit of the last frame must be set explicitly.
+ * WebSocketFrame lastFrame = WebSocketFrame
+ * .{@link WebSocketFrame#createContinuationFrame(String)
+ * createContinuationFrame}("you?")
+ * .{@link WebSocketFrame#setFin(boolean) setFin}(true);
+ *
+ * // Send a text message which consists of 3 frames.
+ * ws.{@link #sendFrame(WebSocketFrame) sendFrame}(firstFrame)
+ * .{@link #sendFrame(WebSocketFrame) sendFrame}(secondFrame)
+ * .{@link #sendFrame(WebSocketFrame) sendFrame}(lastFrame);
+ *
+ *
+ *
+ * Alternatively, the same as above can be done like this.
+ *
+ *
+ *
+ *
// Send a text message which consists of 3 frames.
+ * ws.{@link #sendText(String, boolean) sendText}("How ", false)
+ * .{@link #sendContinuation(String) sendContinuation}("are ")
+ * .{@link #sendContinuation(String, boolean) sendContinuation}("you?", true);
+ *
+ *
+ *
Send Ping/Pong Frames Periodically
+ *
+ *
+ * You can send ping frames periodically by calling {@link #setPingInterval(long)
+ * setPingInterval} method with an interval in milliseconds between ping frames.
+ * This method can be called both before and after {@link #connect()} method.
+ * Passing zero stops the periodical sending.
+ *
+ *
+ *
+ *
// Send a ping per 60 seconds.
+ * ws.{@link #setPingInterval(long) setPingInterval}(60 * 1000);
+ *
+ * // Stop the periodical sending.
+ * ws.{@link #setPingInterval(long) setPingInterval}(0);
+ *
+ *
+ *
+ * Likewise, you can send pong frames periodically by calling {@link
+ * #setPongInterval(long) setPongInterval} method. "A Pong frame MAY be sent
+ * unsolicited." (RFC 6455, 5.5.3. Pong)
+ *
+ *
+ *
+ * You can customize payload of ping/pong frames that are sent automatically by using
+ * {@link #setPingPayloadGenerator(PayloadGenerator)} and
+ * {@link #setPongPayloadGenerator(PayloadGenerator)} methods. Both methods take an
+ * instance of {@link PayloadGenerator} interface. The following is an example to
+ * use the string representation of the current date as payload of ping frames.
+ *
+ *
+ *
+ *
ws.{@link #setPingPayloadGenerator(PayloadGenerator)
+ * setPingPayloadGenerator}(new {@link PayloadGenerator} () {
+ * {@code @}Override
+ * public byte[] generate() {
+ * // The string representation of the current date.
+ * return new Date().toString().getBytes();
+ * }
+ * });
+ *
+ *
+ *
+ * Note that the maximum payload length of control frames (e.g. ping frames) is 125.
+ * Therefore, the length of a byte array returned from {@link PayloadGenerator#generate()
+ * generate()} method must not exceed 125.
+ *
+ *
+ *
Auto Flush
+ *
+ *
+ * By default, a frame is automatically flushed to the server immediately after
+ * {@link #sendFrame(WebSocketFrame) sendFrame} method is executed. This automatic
+ * flush can be disabled by calling {@link #setAutoFlush(boolean) setAutoFlush}{@code
+ * (false)}.
+ *
+ * To flush frames manually, call {@link #flush()} method. Note that this method
+ * works asynchronously.
+ *
+ *
+ *
+ *
// Flush frames to the server manually.
+ * ws.{@link #flush()};
+ *
+ *
+ *
Congestion Control
+ *
+ *
+ * sendXxx methods queue a {@link WebSocketFrame} instance to the
+ * internal queue. By default, no upper limit is imposed on the queue size, so
+ * sendXxx methods do not block. However, this behavior may cause
+ * a problem if your WebSocket client application sends too many WebSocket frames in
+ * a short time for the WebSocket server to process. In such a case, you may want
+ * sendXxx methods to block when many frames are queued.
+ *
+ *
+ *
+ * You can set an upper limit on the internal queue by calling {@link #setFrameQueueSize(int)}
+ * method. As a result, if the number of frames in the queue has reached the upper limit
+ * when a sendXxx method is called, the method blocks until the
+ * queue gets spaces. The code snippet below is an example to set 5 as the upper limit
+ * of the internal frame queue.
+ *
+ *
+ *
+ *
// Set 5 as the frame queue size.
+ * ws.{@link #setFrameQueueSize(int) setFrameQueueSize}(5);
+ *
+ *
+ *
+ * Note that under some conditions, even if the queue is full, sendXxx
+ * methods do not block. For example, in the case where the thread to send frames
+ * ({@code WritingThread}) is going to stop or has already stopped. In addition,
+ * method calls to send a control frame (e.g. {@link #sendClose()} and {@link #sendPing()}) do not block.
+ *
+ *
+ *
Maximum Payload Size
+ *
+ *
+ * You can set an upper limit on the payload size of WebSocket frames by calling
+ * {@link #setMaxPayloadSize(int)} method with a positive value. Text, binary and
+ * continuation frames whose payload size is bigger than the maximum payload size
+ * you have set will be split into multiple frames.
+ *
+ *
+ *
+ *
// Set 1024 as the maximum payload size.
+ * ws.{@link #setMaxPayloadSize(int) setMaxPayloadSize}(1024);
+ *
+ *
+ *
+ * Control frames (close, ping and pong frames) are never split as per the specification.
+ *
+ *
+ *
+ * If permessage-deflate extension is enabled and if the payload size of a WebSocket
+ * frame after compression does not exceed the maximum payload size, the WebSocket
+ * frame is not split even if the payload size before compression execeeds the
+ * maximum payload size.
+ *
+ *
+ *
Compression
+ *
+ *
+ * The permessage-deflate extension (RFC 7692) has been supported
+ * since the version 1.17. To enable the extension, call {@link #addExtension(String)
+ * addExtension} method with {@code "permessage-deflate"}.
+ *
+ * Some server implementations close a WebSocket connection without sending a
+ * close frame to
+ * a client in some cases. Strictly speaking, this is a violation against the
+ * specification (RFC 6455). However, this
+ * library has allowed the behavior by default since the version 1.29. Even if the
+ * end of the input stream of a WebSocket connection were reached without a close
+ * frame being received, it would trigger neither {@link
+ * WebSocketListener#onError(WebSocket, WebSocketException) onError()} method nor
+ * {@link WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame)
+ * onFrameError()} method of {@link WebSocketListener}. If you want to make a
+ * {@code WebSocket} instance report an error in the case, pass {@code false} to
+ * {@link #setMissingCloseFrameAllowed(boolean)} method.
+ *
+ *
+ *
+ *
// Make this library report an error when the end of the input stream
+ * // of the WebSocket connection is reached before a close frame is read.
+ * ws.{@link #setMissingCloseFrameAllowed(boolean) setMissingCloseFrameAllowed}(false);
+ *
+ *
+ *
Disconnect WebSocket
+ *
+ *
+ * Before a WebSocket is closed, a closing handshake is performed. A closing handshake
+ * is started (1) when the server sends a close frame to the client or (2) when the
+ * client sends a close frame to the server. You can start a closing handshake by calling
+ * {@link #disconnect()} method (or by sending a close frame manually).
+ *
+ *
+ *
+ *
// Close the WebSocket connection.
+ * ws.{@link #disconnect()};
+ *
+ *
+ *
+ * {@code disconnect()} method has some variants. If you want to change the close code
+ * and the reason phrase of the close frame that this client will send to the server,
+ * use a variant method such as {@link #disconnect(int, String)}. {@code disconnect()}
+ * method itself is an alias of {@code disconnect(}{@link WebSocketCloseCode}{@code
+ * .NORMAL, null)}.
+ *
+ *
+ *
Reconnection
+ *
+ *
+ * {@code connect()} method can be called at most only once regardless of whether the
+ * method succeeded or failed. If you want to re-connect to the WebSocket endpoint,
+ * you have to create a new {@code WebSocket} instance again by calling one of {@code
+ * createSocket} methods of a {@code WebSocketFactory}. You may find {@link #recreate()}
+ * method useful if you want to create a new {@code WebSocket} instance that has the
+ * same settings as the original instance. Note that, however, settings you made on
+ * the raw socket of the original {@code WebSocket} instance are not copied.
+ *
+ *
+ *
+ *
// Create a new WebSocket instance and connect to the same endpoint.
+ * ws = ws.{@link #recreate()}.{@link #connect()};
+ *
+ *
+ *
+ * There is a variant of {@code recreate()} method that takes a timeout value for
+ * socket connection. If you want to use a timeout value that is different from the
+ * one used when the existing {@code WebSocket} instance was created, use {@link
+ * #recreate(int) recreate(int timeout)} method.
+ *
+ *
+ *
+ * Note that you should not trigger reconnection in {@link
+ * WebSocketListener#onError(WebSocket, WebSocketException) onError()} method
+ * because {@code onError()} may be called multiple times due to one error. Instead,
+ * {@link WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame,
+ * boolean) onDisconnected()} is the right place to trigger reconnection.
+ *
+ *
+ *
+ * Also note that the reason I use an expression of "to trigger reconnection"
+ * instead of "to call recreate().connect()" is that I myself
+ * won't do it synchronously in WebSocketListener callback
+ * methods but will just schedule reconnection or will just go to the top of a kind
+ * of application loop that repeats to establish a WebSocket connection until
+ * it succeeds.
+ *
+ *
+ *
Error Handling
+ *
+ *
+ * {@code WebSocketListener} has some {@code onXxxError()} methods such as {@link
+ * WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame)
+ * onFrameError()} and {@link
+ * WebSocketListener#onSendError(WebSocket, WebSocketException, WebSocketFrame)
+ * onSendError()}. Among such methods, {@link
+ * WebSocketListener#onError(WebSocket, WebSocketException) onError()} is a special
+ * one. It is always called before any other {@code onXxxError()} is called. For
+ * example, in the implementation of {@code run()} method of {@code ReadingThread},
+ * {@code Throwable} is caught and {@code onError()} and {@link
+ * WebSocketListener#onUnexpectedError(WebSocket, WebSocketException)
+ * onUnexpectedError()} are called in this order. The following is the implementation.
+ *
+ *
+ *
+ *
{@code @}Override
+ * public void run()
+ * {
+ * try
+ * {
+ * main();
+ * }
+ * catch (Throwable t)
+ * {
+ * // An uncaught throwable was detected in the reading thread.
+ * {@link WebSocketException} cause = new WebSocketException(
+ * {@link WebSocketError}.{@link WebSocketError#UNEXPECTED_ERROR_IN_READING_THREAD UNEXPECTED_ERROR_IN_READING_THREAD},
+ * "An uncaught throwable was detected in the reading thread", t);
+ *
+ * // Notify the listeners.
+ * ListenerManager manager = mWebSocket.getListenerManager();
+ * manager.callOnError(cause);
+ * manager.callOnUnexpectedError(cause);
+ * }
+ * }
+ *
+ *
+ *
+ * So, you can handle all error cases in {@code onError()} method. However, note
+ * that {@code onError()} may be called multiple times for one error cause, so don't
+ * try to trigger reconnection in {@code onError()}. Instead, {@link
+ * WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame, boolean)
+ * onDiconnected()} is the right place to trigger reconnection.
+ *
+ *
+ *
+ * All {@code onXxxError()} methods receive a {@link WebSocketException} instance
+ * as the second argument (the first argument is a {@code WebSocket} instance). The
+ * exception class provides {@link WebSocketException#getError() getError()} method
+ * which returns a {@link WebSocketError} enum entry. Entries in {@code WebSocketError}
+ * enum are possible causes of errors that may occur in the implementation of this
+ * library. The error causes are so granular that they can make it easy for you to
+ * find the root cause when an error occurs.
+ *
+ *
+ *
+ * {@code Throwable}s thrown by implementations of {@code onXXX()} callback methods
+ * are passed to {@link WebSocketListener#handleCallbackError(WebSocket, Throwable)
+ * handleCallbackError()} of {@code WebSocketListener}.
+ *
Called at the end of the thread's {@code run()} method.
+ *
+ *
+ *
+ *
+ *
+ *
+ * For example, if you want to change the name of the reading thread,
+ * implement {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread)
+ * onThreadCreated()} method like below.
+ *
+ *
+ * @author Takahiko Kawasaki
+ * @see RFC 6455 (The WebSocket Protocol)
+ * @see RFC 7692 (Compression Extensions for WebSocket)
+ * @see [GitHub] nv-websocket-client
+ */
+public class WebSocket {
+ //public static boolean isSecure;
+ public static boolean useMask = true;
+ private static final long DEFAULT_CLOSE_DELAY = 10 * 1000L;
+ private final WebSocketFactory mWebSocketFactory;
+ private final SocketConnector mSocketConnector;
+ private final StateManager mStateManager;
+ private HandshakeBuilder mHandshakeBuilder;
+ private final ListenerManager mListenerManager;
+ private final PingSender mPingSender;
+ private final PongSender mPongSender;
+ private final Object mThreadsLock = new Object();
+ private WebSocketInputStream mInput;
+ private WebSocketOutputStream mOutput;
+ private ReadingThread mReadingThread;
+ private WritingThread mWritingThread;
+ private Map> mServerHeaders;
+ private List mAgreedExtensions;
+ private String mAgreedProtocol;
+ private boolean mExtended;
+ private boolean mAutoFlush = true;
+ private boolean mMissingCloseFrameAllowed = true;
+ private int mFrameQueueSize;
+ private int mMaxPayloadSize;
+ private boolean mOnConnectedCalled;
+ private Object mOnConnectedCalledLock = new Object();
+ private boolean mReadingThreadStarted;
+ private boolean mWritingThreadStarted;
+ private boolean mReadingThreadFinished;
+ private boolean mWritingThreadFinished;
+ private WebSocketFrame mServerCloseFrame;
+ private WebSocketFrame mClientCloseFrame;
+ private PerMessageCompressionExtension mPerMessageCompressionExtension;
+
+
+ WebSocket(WebSocketFactory factory, boolean secure, String userInfo, String host, String path, SocketConnector connector) {
+ mWebSocketFactory = factory;
+ mSocketConnector = connector;
+ mStateManager = new StateManager();
+ mHandshakeBuilder = new HandshakeBuilder(secure, userInfo, host, path);
+ mListenerManager = new ListenerManager(this);
+ mPingSender = new PingSender(this, new CounterPayloadGenerator());
+ mPongSender = new PongSender(this, new CounterPayloadGenerator());
+ }
+
+
+ /**
+ * Create a new {@code WebSocket} instance that has the same settings
+ * as this instance. Note that, however, settings you made on the raw
+ * socket are not copied.
+ *
+ *
+ * The {@link WebSocketFactory} instance that you used to create this
+ * {@code WebSocket} instance is used again.
+ *
+ *
+ *
+ * This method calls {@link #recreate(int)} with the timeout value that
+ * was used when this instance was created. If you want to create a
+ * socket connection with a different timeout value, use {@link
+ * #recreate(int)} method instead.
+ *
+ *
+ * @return A new {@code WebSocket} instance.
+ * @throws IOException {@link WebSocketFactory#createSocket(URI)} threw an exception.
+ * @since 1.6
+ */
+ public WebSocket recreate() throws IOException {
+ return recreate(mSocketConnector.getConnectionTimeout());
+ }
+
+
+ /**
+ * Create a new {@code WebSocket} instance that has the same settings
+ * as this instance. Note that, however, settings you made on the raw
+ * socket are not copied.
+ *
+ *
+ * The {@link WebSocketFactory} instance that you used to create this
+ * {@code WebSocket} instance is used again.
+ *
+ *
+ * @param timeout The timeout value in milliseconds for socket timeout.
+ * A timeout of zero is interpreted as an infinite timeout.
+ * @return A new {@code WebSocket} instance.
+ * @throws IllegalArgumentException The given timeout value is negative.
+ * @throws IOException {@link WebSocketFactory#createSocket(URI)} threw an exception.
+ * @since 1.10
+ */
+ public WebSocket recreate(int timeout) throws IOException {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("The given timeout value is negative.");
+ }
+
+ WebSocket instance = mWebSocketFactory.createSocket(getURI(), timeout);
+
+ // Copy the settings.
+ instance.mHandshakeBuilder = new HandshakeBuilder(mHandshakeBuilder);
+ instance.setPingInterval(getPingInterval());
+ instance.setPongInterval(getPongInterval());
+ instance.setPingPayloadGenerator(getPingPayloadGenerator());
+ instance.setPongPayloadGenerator(getPongPayloadGenerator());
+ instance.mExtended = mExtended;
+ instance.mAutoFlush = mAutoFlush;
+ instance.mMissingCloseFrameAllowed = mMissingCloseFrameAllowed;
+ instance.mFrameQueueSize = mFrameQueueSize;
+
+ // Copy listeners.
+ List listeners = mListenerManager.getListeners();
+ synchronized (listeners) {
+ instance.addListeners(listeners);
+ }
+
+ return instance;
+ }
+
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (isInState(CREATED)) {
+ // The raw socket needs to be closed.
+ finish();
+ }
+
+ super.finalize();
+ }
+
+
+ /**
+ * Get the current state of this WebSocket.
+ *
+ *
+ * The initial state is {@link WebSocketState#CREATED CREATED}.
+ * When {@link #connect()} is called, the state is changed to
+ * {@link WebSocketState#CONNECTING CONNECTING}, and then to
+ * {@link WebSocketState#OPEN OPEN} after a successful opening
+ * handshake. The state is changed to {@link
+ * WebSocketState#CLOSING CLOSING} when a closing handshake
+ * is started, and then to {@link WebSocketState#CLOSED CLOSED}
+ * when the closing handshake finished.
+ *
+ *
+ *
+ * See the description of {@link WebSocketState} for details.
+ *
+ *
+ * @return The current state.
+ * @see WebSocketState
+ */
+ public WebSocketState getState() {
+ synchronized (mStateManager) {
+ return mStateManager.getState();
+ }
+ }
+
+
+ /**
+ * Check if the current state of this WebSocket is {@link
+ * WebSocketState#OPEN OPEN}.
+ *
+ * @return {@code true} if the current state is OPEN.
+ * @since 1.1
+ */
+ public boolean isOpen() {
+ return isInState(OPEN);
+ }
+
+
+ /**
+ * Check if the current state is equal to the specified state.
+ */
+ private boolean isInState(WebSocketState state) {
+ synchronized (mStateManager) {
+ return (mStateManager.getState() == state);
+ }
+ }
+
+
+ /**
+ * Add a value for {@code Sec-WebSocket-Protocol}.
+ *
+ * @param protocol A protocol name.
+ * @return {@code this} object.
+ * @throws IllegalArgumentException The protocol name is invalid. A protocol name must be
+ * a non-empty string with characters in the range U+0021
+ * to U+007E not including separator characters.
+ */
+ public WebSocket addProtocol(String protocol) {
+ mHandshakeBuilder.addProtocol(protocol);
+
+ return this;
+ }
+
+
+ /**
+ * Remove a protocol from {@code Sec-WebSocket-Protocol}.
+ *
+ * @param protocol A protocol name. {@code null} is silently ignored.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket removeProtocol(String protocol) {
+ mHandshakeBuilder.removeProtocol(protocol);
+
+ return this;
+ }
+
+
+ /**
+ * Remove all protocols from {@code Sec-WebSocket-Protocol}.
+ *
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket clearProtocols() {
+ mHandshakeBuilder.clearProtocols();
+
+ return this;
+ }
+
+
+ /**
+ * Add a value for {@code Sec-WebSocket-Extension}.
+ *
+ * @param extension An extension. {@code null} is silently ignored.
+ * @return {@code this} object.
+ */
+ public WebSocket addExtension(WebSocketExtension extension) {
+ mHandshakeBuilder.addExtension(extension);
+
+ return this;
+ }
+
+
+ /**
+ * Add a value for {@code Sec-WebSocket-Extension}. The input string
+ * should comply with the format described in 9.1. Negotiating
+ * Extensions in RFC 6455.
+ *
+ * @param extension A string that represents a WebSocket extension. If it does
+ * not comply with RFC 6455, no value is added to {@code
+ * Sec-WebSocket-Extension}.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket addExtension(String extension) {
+ mHandshakeBuilder.addExtension(extension);
+
+ return this;
+ }
+
+
+ /**
+ * Remove an extension from {@code Sec-WebSocket-Extension}.
+ *
+ * @param extension An extension to remove. {@code null} is silently ignored.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket removeExtension(WebSocketExtension extension) {
+ mHandshakeBuilder.removeExtension(extension);
+
+ return this;
+ }
+
+
+ /**
+ * Remove extensions from {@code Sec-WebSocket-Extension} by
+ * an extension name.
+ *
+ * @param name An extension name. {@code null} is silently ignored.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket removeExtensions(String name) {
+ mHandshakeBuilder.removeExtensions(name);
+
+ return this;
+ }
+
+
+ /**
+ * Remove all extensions from {@code Sec-WebSocket-Extension}.
+ *
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket clearExtensions() {
+ mHandshakeBuilder.clearExtensions();
+
+ return this;
+ }
+
+
+ /**
+ * Add a pair of extra HTTP header.
+ *
+ * @param name An HTTP header name. When {@code null} or an empty
+ * string is given, no header is added.
+ * @param value The value of the HTTP header.
+ * @return {@code this} object.
+ */
+ public WebSocket addHeader(String name, String value) {
+ mHandshakeBuilder.addHeader(name, value);
+
+ return this;
+ }
+
+
+ /**
+ * Remove pairs of extra HTTP headers.
+ *
+ * @param name An HTTP header name. {@code null} is silently ignored.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket removeHeaders(String name) {
+ mHandshakeBuilder.removeHeaders(name);
+
+ return this;
+ }
+
+
+ /**
+ * Clear all extra HTTP headers.
+ *
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket clearHeaders() {
+ mHandshakeBuilder.clearHeaders();
+
+ return this;
+ }
+
+
+ /**
+ * Set the credentials to connect to the WebSocket endpoint.
+ *
+ * @param userInfo The credentials for Basic Authentication. The format
+ * should be id:password.
+ * @return {@code this} object.
+ */
+ public WebSocket setUserInfo(String userInfo) {
+ mHandshakeBuilder.setUserInfo(userInfo);
+
+ return this;
+ }
+
+
+ /**
+ * Set the credentials to connect to the WebSocket endpoint.
+ *
+ * @param id The ID.
+ * @param password The password.
+ * @return {@code this} object.
+ */
+ public WebSocket setUserInfo(String id, String password) {
+ mHandshakeBuilder.setUserInfo(id, password);
+
+ return this;
+ }
+
+
+ /**
+ * Clear the credentials to connect to the WebSocket endpoint.
+ *
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket clearUserInfo() {
+ mHandshakeBuilder.clearUserInfo();
+
+ return this;
+ }
+
+
+ /**
+ * Check if extended use of WebSocket frames are allowed.
+ *
+ *
+ * When extended use is allowed, values of RSV1/RSV2/RSV3 bits
+ * and opcode of frames are not checked. On the other hand,
+ * if not allowed (default), non-zero values for RSV1/RSV2/RSV3
+ * bits and unknown opcodes cause an error. In such a case,
+ * {@link WebSocketListener#onFrameError(WebSocket,
+ * WebSocketException, WebSocketFrame) onFrameError} method of
+ * listeners are called and the WebSocket is eventually closed.
+ *
+ *
+ * @return {@code true} if extended use of WebSocket frames
+ * are allowed.
+ */
+ public boolean isExtended() {
+ return mExtended;
+ }
+
+
+ /**
+ * Allow or disallow extended use of WebSocket frames.
+ *
+ * @param extended {@code true} to allow extended use of WebSocket frames.
+ * @return {@code this} object.
+ */
+ public WebSocket setExtended(boolean extended) {
+ mExtended = extended;
+
+ return this;
+ }
+
+
+ /**
+ * Check if flush is performed automatically after {@link
+ * #sendFrame(WebSocketFrame)} is done. The default value is
+ * {@code true}.
+ *
+ * @return {@code true} if flush is performed automatically.
+ * @since 1.5
+ */
+ public boolean isAutoFlush() {
+ return mAutoFlush;
+ }
+
+
+ /**
+ * Enable or disable auto-flush of sent frames.
+ *
+ * @param auto {@code true} to enable auto-flush. {@code false} to
+ * disable it.
+ * @return {@code this} object.
+ * @since 1.5
+ */
+ public WebSocket setAutoFlush(boolean auto) {
+ mAutoFlush = auto;
+
+ return this;
+ }
+
+
+ /**
+ * Check if this instance allows the server to close the WebSocket
+ * connection without sending a close frame
+ * to this client. The default value is {@code true}.
+ *
+ * @return {@code true} if the configuration allows for the server to
+ * close the WebSocket connection without sending a close frame
+ * to this client. {@code false} if the configuration requires
+ * that an error be reported via
+ * {@link WebSocketListener#onError(WebSocket, WebSocketException)
+ * onError()} method and {@link WebSocketListener#onFrameError(WebSocket,
+ * WebSocketException, WebSocketFrame) onFrameError()} method of
+ * {@link WebSocketListener}.
+ * @since 1.29
+ */
+ public boolean isMissingCloseFrameAllowed() {
+ return mMissingCloseFrameAllowed;
+ }
+
+
+ /**
+ * Set whether to allow the server to close the WebSocket connection
+ * without sending a close frame
+ * to this client.
+ *
+ * @param allowed {@code true} to allow the server to close the WebSocket
+ * connection without sending a close frame to this client.
+ * {@code false} to make this instance report an error when the
+ * end of the input stream of the WebSocket connection is reached
+ * before a close frame is read.
+ * @return {@code this} object.
+ * @since 1.29
+ */
+ public WebSocket setMissingCloseFrameAllowed(boolean allowed) {
+ mMissingCloseFrameAllowed = allowed;
+
+ return this;
+ }
+
+
+ /**
+ * Flush frames to the server. Flush is performed asynchronously.
+ *
+ * @return {@code this} object.
+ * @since 1.5
+ */
+ public WebSocket flush() {
+ synchronized (mStateManager) {
+ WebSocketState state = mStateManager.getState();
+
+ if (state != OPEN && state != CLOSING) {
+ return this;
+ }
+ }
+
+ // Get the reference to the instance of WritingThread.
+ WritingThread wt = mWritingThread;
+
+ // If and only if an instance of WritingThread is available.
+ if (wt != null) {
+ // Request flush.
+ wt.queueFlush();
+ }
+
+ return this;
+ }
+
+
+ /**
+ * Get the size of the frame queue. The default value is 0 and it means
+ * there is no limit on the queue size.
+ *
+ * @return The size of the frame queue.
+ * @since 1.15
+ */
+ public int getFrameQueueSize() {
+ return mFrameQueueSize;
+ }
+
+
+ /**
+ * Set the size of the frame queue. The default value is 0 and it means
+ * there is no limit on the queue size.
+ *
+ *
+ * sendXxx methods queue a {@link WebSocketFrame}
+ * instance to the internal queue. If the number of frames in the queue
+ * has reached the upper limit (which has been set by this method) when
+ * a sendXxx method is called, the method blocks
+ * until the queue gets spaces.
+ *
+ *
+ *
+ * Under some conditions, even if the queue is full, sendXxx
+ * methods do not block. For example, in the case where the thread to send
+ * frames ({@code WritingThread}) is going to stop or has already stopped.
+ * In addition, method calls to send a control frame (e.g.
+ * {@link #sendClose()} and {@link #sendPing()}) do not block.
+ *
+ *
+ * @param size The queue size. 0 means no limit. Negative numbers are not allowed.
+ * @return {@code this} object.
+ * @throws IllegalArgumentException {@code size} is negative.
+ * @since 1.15
+ */
+ public WebSocket setFrameQueueSize(int size) throws IllegalArgumentException {
+ if (size < 0) {
+ throw new IllegalArgumentException("size must not be negative.");
+ }
+
+ mFrameQueueSize = size;
+
+ return this;
+ }
+
+
+ /**
+ * Get the maximum payload size. The default value is 0 which means that
+ * the maximum payload size is not set and as a result frames are not split.
+ *
+ * @return The maximum payload size. 0 means that the maximum payload size
+ * is not set.
+ * @since 1.27
+ */
+ public int getMaxPayloadSize() {
+ return mMaxPayloadSize;
+ }
+
+
+ /**
+ * Set the maximum payload size.
+ *
+ *
+ * Text, binary and continuation frames whose payload size is bigger than
+ * the maximum payload size will be split into multiple frames. Note that
+ * control frames (close, ping and pong frames) are not split as per the
+ * specification even if their payload size exceeds the maximum payload size.
+ *
+ *
+ * @param size The maximum payload size. 0 to unset the maximum payload size.
+ * @return {@code this} object.
+ * @throws IllegalArgumentException {@code size} is negative.
+ * @since 1.27
+ */
+ public WebSocket setMaxPayloadSize(int size) throws IllegalArgumentException {
+ if (size < 0) {
+ throw new IllegalArgumentException("size must not be negative.");
+ }
+
+ mMaxPayloadSize = size;
+
+ return this;
+ }
+
+
+ /**
+ * Get the interval of periodical
+ * ping
+ * frames.
+ *
+ * @return The interval in milliseconds.
+ * @since 1.2
+ */
+ public long getPingInterval() {
+ return mPingSender.getInterval();
+ }
+
+
+ /**
+ * Set the interval of periodical
+ * ping
+ * frames.
+ *
+ *
+ * Setting a positive number starts sending ping frames periodically.
+ * Setting zero stops the periodical sending. This method can be called
+ * both before and after {@link #connect()} method.
+ *
+ *
+ * @param interval The interval in milliseconds. A negative value is
+ * regarded as zero.
+ * @return {@code this} object.
+ * @since 1.2
+ */
+ public WebSocket setPingInterval(long interval) {
+ mPingSender.setInterval(interval);
+
+ return this;
+ }
+
+
+ /**
+ * Get the interval of periodical
+ * pong
+ * frames.
+ *
+ * @return The interval in milliseconds.
+ * @since 1.2
+ */
+ public long getPongInterval() {
+ return mPongSender.getInterval();
+ }
+
+
+ /**
+ * Set the interval of periodical
+ * pong
+ * frames.
+ *
+ *
+ * Setting a positive number starts sending pong frames periodically.
+ * Setting zero stops the periodical sending. This method can be called
+ * both before and after {@link #connect()} method.
+ *
+ * A Pong frame MAY be sent unsolicited. This serves as a
+ * unidirectional heartbeat. A response to an unsolicited Pong
+ * frame is not expected.
+ *
+ *
+ *
+ *
+ *
+ * @param interval The interval in milliseconds. A negative value is
+ * regarded as zero.
+ * @return {@code this} object.
+ * @since 1.2
+ */
+ public WebSocket setPongInterval(long interval) {
+ mPongSender.setInterval(interval);
+
+ return this;
+ }
+
+
+ /**
+ * Get the generator of payload of ping frames that are sent automatically.
+ *
+ * @return The generator of payload ping frames that are sent automatically.
+ * @since 1.20
+ */
+ public PayloadGenerator getPingPayloadGenerator() {
+ return mPingSender.getPayloadGenerator();
+ }
+
+
+ /**
+ * Set the generator of payload of ping frames that are sent automatically.
+ *
+ * @param generator The generator of payload ping frames that are sent automatically.
+ * @since 1.20
+ */
+ public WebSocket setPingPayloadGenerator(PayloadGenerator generator) {
+ mPingSender.setPayloadGenerator(generator);
+
+ return this;
+ }
+
+
+ /**
+ * Get the generator of payload of pong frames that are sent automatically.
+ *
+ * @return The generator of payload pong frames that are sent automatically.
+ * @since 1.20
+ */
+ public PayloadGenerator getPongPayloadGenerator() {
+ return mPongSender.getPayloadGenerator();
+ }
+
+
+ /**
+ * Set the generator of payload of pong frames that are sent automatically.
+ *
+ * @param generator The generator of payload ppng frames that are sent automatically.
+ * @since 1.20
+ */
+ public WebSocket setPongPayloadGenerator(PayloadGenerator generator) {
+ mPongSender.setPayloadGenerator(generator);
+
+ return this;
+ }
+
+
+ /**
+ * Add a listener to receive events on this WebSocket.
+ *
+ * @param listener A listener to add.
+ * @return {@code this} object.
+ */
+ public WebSocket addListener(WebSocketListener listener) {
+ mListenerManager.addListener(listener);
+
+ return this;
+ }
+
+
+ /**
+ * Add listeners.
+ *
+ * @param listeners Listeners to add. {@code null} is silently ignored.
+ * {@code null} elements in the list are ignored, too.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket addListeners(List listeners) {
+ mListenerManager.addListeners(listeners);
+
+ return this;
+ }
+
+
+ /**
+ * Remove a listener from this WebSocket.
+ *
+ * @param listener A listener to remove. {@code null} won't cause an error.
+ * @return {@code this} object.
+ * @since 1.13
+ */
+ public WebSocket removeListener(WebSocketListener listener) {
+ mListenerManager.removeListener(listener);
+
+ return this;
+ }
+
+
+ /**
+ * Remove listeners.
+ *
+ * @param listeners Listeners to remove. {@code null} is silently ignored.
+ * {@code null} elements in the list are ignored, too.
+ * @return {@code this} object.
+ * @since 1.14
+ */
+ public WebSocket removeListeners(List listeners) {
+ mListenerManager.removeListeners(listeners);
+
+ return this;
+ }
+
+
+ /**
+ * Remove all the listeners from this WebSocket.
+ *
+ * @return {@code this} object.
+ * @since 1.13
+ */
+ public WebSocket clearListeners() {
+ mListenerManager.clearListeners();
+
+ return this;
+ }
+
+
+ /**
+ * Get the raw socket which this WebSocket uses internally.
+ *
+ * @return The underlying {@link Socket} instance.
+ */
+ public Socket getSocket() {
+ return mSocketConnector.getSocket();
+ }
+
+
+ /**
+ * Get the URI of the WebSocket endpoint. The scheme part is either
+ * {@code "ws"} or {@code "wss"}. The authority part is always empty.
+ *
+ * @return The URI of the WebSocket endpoint.
+ * @since 1.1
+ */
+ public URI getURI() {
+ return mHandshakeBuilder.getURI();
+ }
+
+
+ /**
+ * Connect to the server, send an opening handshake to the server,
+ * receive the response and then start threads to communicate with
+ * the server.
+ *
+ *
+ * As necessary, {@link #addProtocol(String)}, {@link #addExtension(WebSocketExtension)}
+ * {@link #addHeader(String, String)} should be called before you call this
+ * method. It is because the parameters set by these methods are used in the
+ * opening handshake.
+ *
+ *
+ *
+ * Also, as necessary, {@link #getSocket()} should be used to set up socket
+ * parameters before you call this method. For example, you can set the
+ * socket timeout like the following.
+ *
+ * If the WebSocket endpoint requires Basic Authentication, you can set
+ * credentials by {@link #setUserInfo(String) setUserInfo(userInfo)} or
+ * {@link #setUserInfo(String, String) setUserInfo(id, password)} before
+ * you call this method.
+ * Note that if the URI passed to {@link WebSocketFactory}{@code
+ * .createSocket} method contains the user-info part, you don't have to
+ * call {@code setUserInfo} method.
+ *
+ *
+ *
+ * Note that this method can be called at most only once regardless of
+ * whether this method succeeded or failed. If you want to re-connect to
+ * the WebSocket endpoint, you have to create a new {@code WebSocket}
+ * instance again by calling one of {@code createSocket} methods of a
+ * {@link WebSocketFactory}. You may find {@link #recreate()} method
+ * useful if you want to create a new {@code WebSocket} instance that
+ * has the same settings as this instance. (But settings you made on
+ * the raw socket are not copied.)
+ *
The current state of the WebSocket is not {@link
+ * WebSocketState#CREATED CREATED}
+ *
Connecting the server failed.
+ *
The opening handshake failed.
+ *
+ */
+ public WebSocket connect() throws WebSocketException {
+ // Change the state to CONNECTING. If the state before
+ // the change is not CREATED, an exception is thrown.
+ changeStateOnConnect();
+
+ // HTTP headers from the server.
+ Map> headers;
+
+ try {
+ // Connect to the server.
+ mSocketConnector.connect();
+
+ // Perform WebSocket handshake.
+ headers = shakeHands();
+ } catch (WebSocketException e) {
+ // Close the socket.
+ mSocketConnector.closeSilently();
+
+ // Change the state to CLOSED.
+ mStateManager.setState(CLOSED);
+
+ // Notify the listener of the state change.
+ mListenerManager.callOnStateChanged(CLOSED);
+
+ // The handshake failed.
+ throw e;
+ }
+
+ // HTTP headers in the response from the server.
+ mServerHeaders = headers;
+
+ // Extensions.
+ mPerMessageCompressionExtension = findAgreedPerMessageCompressionExtension();
+
+ // Change the state to OPEN.
+ mStateManager.setState(OPEN);
+
+ // Notify the listener of the state change.
+ mListenerManager.callOnStateChanged(OPEN);
+
+ // Start threads that communicate with the server.
+ startThreads();
+
+ return this;
+ }
+
+
+ /**
+ * Execute {@link #connect()} asynchronously using the given {@link
+ * ExecutorService}. This method is just an alias of the following.
+ *
+ *
+ * This method is an alias of {@link #disconnect(int, String)
+ * disconnect}{@code (closeCode, null)}.
+ *
+ *
+ * @param closeCode The close code embedded in a close frame
+ * which this WebSocket client will send to the server.
+ * @return {@code this} object.
+ * @since 1.5
+ */
+ public WebSocket disconnect(int closeCode) {
+ return disconnect(closeCode, null);
+ }
+
+
+ /**
+ * Disconnect the WebSocket.
+ *
+ *
+ * This method is an alias of {@link #disconnect(int, String)
+ * disconnect}{@code (}{@link WebSocketCloseCode#NORMAL}{@code , reason)}.
+ *
+ *
+ * @param reason The reason embedded in a close frame
+ * which this WebSocket client will send to the server. Note that
+ * the length of the bytes which represents the given reason must
+ * not exceed 125. In other words, {@code (reason.}{@link
+ * String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
+ * must be true.
+ * @return {@code this} object.
+ * @since 1.5
+ */
+ public WebSocket disconnect(String reason) {
+ return disconnect(WebSocketCloseCode.NORMAL, reason);
+ }
+
+
+ /**
+ * Disconnect the WebSocket.
+ *
+ *
+ * This method is an alias of {@link #disconnect(int, String, long)
+ * disconnect}{@code (closeCode, reason, 10000L)}.
+ *
+ *
+ * @param closeCode The close code embedded in a close frame
+ * which this WebSocket client will send to the server.
+ * @param reason The reason embedded in a close frame
+ * which this WebSocket client will send to the server. Note that
+ * the length of the bytes which represents the given reason must
+ * not exceed 125. In other words, {@code (reason.}{@link
+ * String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
+ * must be true.
+ * @return {@code this} object.
+ * @see WebSocketCloseCode
+ * @see RFC 6455, 5.5.1. Close
+ * @since 1.5
+ */
+ public WebSocket disconnect(int closeCode, String reason) {
+ return disconnect(closeCode, reason, DEFAULT_CLOSE_DELAY);
+ }
+
+
+ /**
+ * Disconnect the WebSocket.
+ *
+ * @param closeCode The close code embedded in a close frame
+ * which this WebSocket client will send to the server.
+ * @param reason The reason embedded in a close frame
+ * which this WebSocket client will send to the server. Note that
+ * the length of the bytes which represents the given reason must
+ * not exceed 125. In other words, {@code (reason.}{@link
+ * String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
+ * must be true.
+ * @param closeDelay Delay in milliseconds before calling {@link Socket#close()} forcibly.
+ * This safeguard is needed for the case where the server fails to send
+ * back a close frame. The default value is 10000 (= 10 seconds). When
+ * a negative value is given, the default value is used.
+ *
+ * If a very short time (e.g. 0) is given, it is likely to happen either
+ * (1) that this client will fail to send a close frame to the server
+ * (in this case, you will probably see an error message "Flushing frames
+ * to the server failed: Socket closed") or (2) that the WebSocket
+ * connection will be closed before this client receives a close frame
+ * from the server (in this case, the second argument of {@link
+ * WebSocketListener#onDisconnected(WebSocket, WebSocketFrame,
+ * WebSocketFrame, boolean) WebSocketListener.onDisconnected} will be
+ * {@code null}).
+ * @return {@code this} object.
+ * @see WebSocketCloseCode
+ * @see RFC 6455, 5.5.1. Close
+ * @since 1.26
+ */
+ public WebSocket disconnect(int closeCode, String reason, long closeDelay) {
+ synchronized (mStateManager) {
+ switch (mStateManager.getState()) {
+ case CREATED:
+ finishAsynchronously();
+ return this;
+
+ case OPEN:
+ break;
+
+ default:
+ // - CONNECTING
+ // It won't happen unless the programmer dare call
+ // open() and disconnect() in parallel.
+ //
+ // - CLOSING
+ // A closing handshake has already been started.
+ //
+ // - CLOSED
+ // The connection has already been closed.
+ return this;
+ }
+
+ // Change the state to CLOSING.
+ mStateManager.changeToClosing(CloseInitiator.CLIENT);
+
+ // Create a close frame.
+ WebSocketFrame frame = WebSocketFrame.createCloseFrame(closeCode, reason);
+
+ // Send the close frame to the server.
+ sendFrame(frame);
+ }
+
+ // Notify the listeners of the state change.
+ mListenerManager.callOnStateChanged(CLOSING);
+
+ // If a negative value is given.
+ if (closeDelay < 0) {
+ // Use the default value.
+ closeDelay = DEFAULT_CLOSE_DELAY;
+ }
+
+ // Request the threads to stop.
+ stopThreads(closeDelay);
+
+ return this;
+ }
+
+
+ /**
+ * Get the agreed extensions.
+ *
+ *
+ * This method works correctly only after {@link #connect()} succeeds
+ * (= after the opening handshake succeeds).
+ *
+ *
+ * @return The agreed extensions.
+ */
+ public List getAgreedExtensions() {
+ return mAgreedExtensions;
+ }
+
+
+ /**
+ * Get the agreed protocol.
+ *
+ *
+ * This method works correctly only after {@link #connect()} succeeds
+ * (= after the opening handshake succeeds).
+ *
+ *
+ * @return The agreed protocol.
+ */
+ public String getAgreedProtocol() {
+ return mAgreedProtocol;
+ }
+
+
+ /**
+ * Send a WebSocket frame to the server.
+ *
+ *
+ * This method just queues the given frame. Actual transmission
+ * is performed asynchronously.
+ *
+ *
+ *
+ * When the current state of this WebSocket is not {@link
+ * WebSocketState#OPEN OPEN}, this method does not accept
+ * the frame.
+ *
+ *
+ *
+ * Sending a close frame changes the state to {@link WebSocketState#CLOSING
+ * CLOSING} (if the current state is neither {@link WebSocketState#CLOSING
+ * CLOSING} nor {@link WebSocketState#CLOSED CLOSED}).
+ *
+ *
+ *
+ * Note that the validity of the give frame is not checked.
+ * For example, even if the payload length of a given frame
+ * is greater than 125 and the opcode indicates that the
+ * frame is a control frame, this method accepts the given
+ * frame.
+ *
+ *
+ * @param frame A WebSocket frame to be sent to the server.
+ * If {@code null} is given, nothing is done.
+ * @return {@code this} object.
+ */
+ public WebSocket sendFrame(WebSocketFrame frame) {
+ if (frame == null) {
+ return this;
+ }
+
+ synchronized (mStateManager) {
+ WebSocketState state = mStateManager.getState();
+
+ if (state != OPEN && state != CLOSING) {
+ return this;
+ }
+ }
+
+ // The current state is either OPEN or CLOSING. Or, CLOSED.
+
+ // Get the reference to the writing thread.
+ WritingThread wt = mWritingThread;
+
+ // Some applications call sendFrame() without waiting for the
+ // notification of WebSocketListener.onConnected() (Issue #23),
+ // and/or even after the connection is closed. That is, there
+ // are chances that sendFrame() is called when mWritingThread
+ // is null. So, it should be checked whether an instance of
+ // WritingThread is available or not before calling queueFrame().
+ if (wt == null) {
+ // An instance of WritingThread is not available.
+ return this;
+ }
+
+ // Split the frame into multiple frames if necessary.
+ List frames = splitIfNecessary(frame);
+
+ // Queue the frame or the frames. Even if the current state is
+ // CLOSED, queueing won't be a big issue.
+
+ // If the frame was not split.
+ if (frames == null) {
+ // Queue the frame.
+ wt.queueFrame(frame);
+ } else {
+ for (WebSocketFrame f : frames) {
+ // Queue the frame.
+ wt.queueFrame(f);
+ }
+ }
+
+ return this;
+ }
+
+
+ private List splitIfNecessary(WebSocketFrame frame) {
+ return WebSocketFrame.splitIfNecessary(frame, mMaxPayloadSize, mPerMessageCompressionExtension);
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame()
+ * createContinuationFrame()}{@code )}.
+ *
+ *
+ *
+ * Note that the FIN bit of a frame sent by this method is {@code false}.
+ * If you want to set the FIN bit, use {@link #sendContinuation(boolean)
+ * sendContinuation(boolean fin)} with {@code fin=true}.
+ *
+ *
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation() {
+ return sendFrame(WebSocketFrame.createContinuationFrame());
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame()
+ * createContinuationFrame()}{@code .}{@link
+ * WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
+ *
+ *
+ * @param fin The FIN bit value.
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation(boolean fin) {
+ return sendFrame(WebSocketFrame.createContinuationFrame().setFin(fin));
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame(String)
+ * createContinuationFrame}{@code (payload))}.
+ *
+ *
+ *
+ * Note that the FIN bit of a frame sent by this method is {@code false}.
+ * If you want to set the FIN bit, use {@link #sendContinuation(String,
+ * boolean) sendContinuation(String payload, boolean fin)} with {@code
+ * fin=true}.
+ *
+ *
+ * @param payload The payload of a continuation frame.
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation(String payload) {
+ return sendFrame(WebSocketFrame.createContinuationFrame(payload));
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame(String)
+ * createContinuationFrame}{@code (payload).}{@link
+ * WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
+ *
+ *
+ * @param payload The payload of a continuation frame.
+ * @param fin The FIN bit value.
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation(String payload, boolean fin) {
+ return sendFrame(WebSocketFrame.createContinuationFrame(payload).setFin(fin));
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame(byte[])
+ * createContinuationFrame}{@code (payload))}.
+ *
+ *
+ *
+ * Note that the FIN bit of a frame sent by this method is {@code false}.
+ * If you want to set the FIN bit, use {@link #sendContinuation(byte[],
+ * boolean) sendContinuation(byte[] payload, boolean fin)} with {@code
+ * fin=true}.
+ *
+ *
+ * @param payload The payload of a continuation frame.
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation(byte[] payload) {
+ return sendFrame(WebSocketFrame.createContinuationFrame(payload));
+ }
+
+
+ /**
+ * Send a continuation frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createContinuationFrame(byte[])
+ * createContinuationFrame}{@code (payload).}{@link
+ * WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
+ *
+ *
+ * @param payload The payload of a continuation frame.
+ * @param fin The FIN bit value.
+ * @return {@code this} object.
+ */
+ public WebSocket sendContinuation(byte[] payload, boolean fin) {
+ return sendFrame(WebSocketFrame.createContinuationFrame(payload).setFin(fin));
+ }
+
+
+ /**
+ * Send a text message to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createTextFrame(String)
+ * createTextFrame}{@code (message))}.
+ *
+ *
+ *
+ * If you want to send a text frame that is to be followed by
+ * continuation frames, use {@link #sendText(String, boolean)
+ * setText(String payload, boolean fin)} with {@code fin=false}.
+ *
+ *
+ * @param message A text message to be sent to the server.
+ * @return {@code this} object.
+ */
+ public WebSocket sendText(String message) {
+ return sendFrame(WebSocketFrame.createTextFrame(message));
+ }
+
+
+ /**
+ * Send a text frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createTextFrame(String)
+ * createTextFrame}{@code (payload).}{@link
+ * WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
+ *
+ *
+ * @param payload The payload of a text frame.
+ * @param fin The FIN bit value.
+ * @return {@code this} object.
+ */
+ public WebSocket sendText(String payload, boolean fin) {
+ return sendFrame(WebSocketFrame.createTextFrame(payload).setFin(fin));
+ }
+
+
+ /**
+ * Send a binary message to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createBinaryFrame(byte[])
+ * createBinaryFrame}{@code (message))}.
+ *
+ *
+ *
+ * If you want to send a binary frame that is to be followed by
+ * continuation frames, use {@link #sendBinary(byte[], boolean)
+ * setBinary(byte[] payload, boolean fin)} with {@code fin=false}.
+ *
+ *
+ * @param message A binary message to be sent to the server.
+ * @return {@code this} object.
+ */
+ public WebSocket sendBinary(byte[] message, Object requestWrapper) {
+ return sendFrame(WebSocketFrame.createBinaryFrame(message, requestWrapper));
+ }
+
+
+ /**
+ * Send a binary frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createBinaryFrame(byte[])
+ * createBinaryFrame}{@code (payload).}{@link
+ * WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
+ *
+ *
+ * @param payload The payload of a binary frame.
+ * @param fin The FIN bit value.
+ * @return {@code this} object.
+ */
+ public WebSocket sendBinary(byte[] payload, boolean fin) {
+ return sendFrame(WebSocketFrame.createBinaryFrame(payload).setFin(fin));
+ }
+
+
+ /**
+ * Send a close frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createCloseFrame() createCloseFrame()}).
+ *
+ *
+ * @return {@code this} object.
+ */
+ public WebSocket sendClose() {
+ return sendFrame(WebSocketFrame.createCloseFrame());
+ }
+
+
+ /**
+ * Send a close frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createCloseFrame(int)
+ * createCloseFrame}{@code (closeCode))}.
+ *
+ *
+ * @param closeCode The close code.
+ * @return {@code this} object.
+ * @see WebSocketCloseCode
+ */
+ public WebSocket sendClose(int closeCode) {
+ return sendFrame(WebSocketFrame.createCloseFrame(closeCode));
+ }
+
+
+ /**
+ * Send a close frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createCloseFrame(int, String)
+ * createCloseFrame}{@code (closeCode, reason))}.
+ *
+ *
+ * @param closeCode The close code.
+ * @param reason The close reason.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return {@code this} object.
+ * @see WebSocketCloseCode
+ */
+ public WebSocket sendClose(int closeCode, String reason) {
+ return sendFrame(WebSocketFrame.createCloseFrame(closeCode, reason));
+ }
+
+
+ /**
+ * Send a ping frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPingFrame() createPingFrame()}).
+ *
+ *
+ * @return {@code this} object.
+ */
+ public WebSocket sendPing() {
+ return sendFrame(WebSocketFrame.createPingFrame());
+ }
+
+
+ /**
+ * Send a ping frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPingFrame(byte[])
+ * createPingFrame}{@code (payload))}.
+ *
+ *
+ * @param payload The payload for a ping frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return {@code this} object.
+ */
+ public WebSocket sendPing(byte[] payload) {
+ return sendFrame(WebSocketFrame.createPingFrame(payload));
+ }
+
+
+ /**
+ * Send a ping frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPingFrame(String)
+ * createPingFrame}{@code (payload))}.
+ *
+ *
+ * @param payload The payload for a ping frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return {@code this} object.
+ */
+ public WebSocket sendPing(String payload) {
+ return sendFrame(WebSocketFrame.createPingFrame(payload));
+ }
+
+
+ /**
+ * Send a pong frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPongFrame() createPongFrame()}).
+ *
+ *
+ * @return {@code this} object.
+ */
+ public WebSocket sendPong() {
+ return sendFrame(WebSocketFrame.createPongFrame());
+ }
+
+
+ /**
+ * Send a pong frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPongFrame(byte[])
+ * createPongFrame}{@code (payload))}.
+ *
+ *
+ * @param payload The payload for a pong frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return {@code this} object.
+ */
+ public WebSocket sendPong(byte[] payload) {
+ return sendFrame(WebSocketFrame.createPongFrame(payload));
+ }
+
+
+ /**
+ * Send a pong frame to the server.
+ *
+ *
+ * This method is an alias of {@link #sendFrame(WebSocketFrame)
+ * sendFrame}{@code (WebSocketFrame.}{@link
+ * WebSocketFrame#createPongFrame(String)
+ * createPongFrame}{@code (payload))}.
+ *
+ *
+ * @param payload The payload for a pong frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return {@code this} object.
+ */
+ public WebSocket sendPong(String payload) {
+ return sendFrame(WebSocketFrame.createPongFrame(payload));
+ }
+
+
+ private void changeStateOnConnect() throws WebSocketException {
+ synchronized (mStateManager) {
+ // If the current state is not CREATED.
+ if (mStateManager.getState() != CREATED) {
+ throw new WebSocketException(WebSocketError.NOT_IN_CREATED_STATE, "The current state of the WebSocket is not CREATED.");
+ }
+
+ // Change the state to CONNECTING.
+ mStateManager.setState(CONNECTING);
+ }
+
+ // Notify the listeners of the state change.
+ mListenerManager.callOnStateChanged(CONNECTING);
+ }
+
+
+ /**
+ * Perform the opening handshake.
+ */
+ private Map> shakeHands() throws WebSocketException {
+ // The raw socket created by WebSocketFactory.
+ Socket socket = mSocketConnector.getSocket();
+
+ // Get the input stream of the socket.
+ WebSocketInputStream input = openInputStream(socket);
+
+ // Get the output stream of the socket.
+ WebSocketOutputStream output = openOutputStream(socket);
+
+ // Generate a value for Sec-WebSocket-Key.
+ String key = generateWebSocketKey();
+
+ // Send an opening handshake to the server.
+ writeHandshake(output, key);
+
+ // Read the response from the server.
+ Map> headers = readHandshake(input, key);
+
+ // Keep the input stream and the output stream to pass them
+ // to the reading thread and the writing thread later.
+ mInput = input;
+ mOutput = output;
+
+ // The handshake succeeded.
+ return headers;
+ }
+
+
+ /**
+ * Open the input stream of the WebSocket connection.
+ * The stream is used by the reading thread.
+ */
+ private WebSocketInputStream openInputStream(Socket socket) throws WebSocketException {
+ try {
+ // Get the input stream of the raw socket through which
+ // this client receives data from the server.
+ return new WebSocketInputStream(new BufferedInputStream(socket.getInputStream()));
+ } catch (IOException e) {
+ // Failed to get the input stream of the raw socket.
+ throw new WebSocketException(WebSocketError.SOCKET_INPUT_STREAM_FAILURE, "Failed to get the input stream of the raw socket: " + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * Open the output stream of the WebSocket connection.
+ * The stream is used by the writing thread.
+ */
+ private WebSocketOutputStream openOutputStream(Socket socket) throws WebSocketException {
+ try {
+ // Get the output stream of the socket through which
+ // this client sends data to the server.
+ return new WebSocketOutputStream(new BufferedOutputStream(socket.getOutputStream()));
+ } catch (IOException e) {
+ // Failed to get the output stream from the raw socket.
+ throw new WebSocketException(WebSocketError.SOCKET_OUTPUT_STREAM_FAILURE, "Failed to get the output stream from the raw socket: " + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * Generate a value for Sec-WebSocket-Key.
+ *
+ *
+ *
+ * The request MUST include a header field with the name Sec-WebSocket-Key.
+ * The value of this header field MUST be a nonce consisting of a randomly
+ * selected 16-byte value that has been base64-encoded (see Section 4 of
+ * RFC 4648). The nonce MUST be selected randomly for each connection.
+ *
+ *
+ *
+ * @return A randomly generated WebSocket key.
+ */
+ private static String generateWebSocketKey() {
+ // "16-byte value"
+ byte[] data = new byte[16];
+
+ // "randomly selected"
+ Misc.nextBytes(data);
+
+ // "base64-encoded"
+ return Base64.encode(data);
+ }
+
+
+ /**
+ * Send an opening handshake request to the WebSocket server.
+ */
+ private void writeHandshake(WebSocketOutputStream output, String key) throws WebSocketException {
+ // Generate an opening handshake sent to the server from this client.
+ mHandshakeBuilder.setKey(key);
+ String requestLine = mHandshakeBuilder.buildRequestLine();
+ List headers = mHandshakeBuilder.buildHeaders();
+ String handshake = HandshakeBuilder.build(requestLine, headers);
+
+ // Call onSendingHandshake() method of listeners.
+ mListenerManager.callOnSendingHandshake(requestLine, headers);
+
+ try {
+ // Send the opening handshake to the server.
+ output.write(handshake);
+ output.flush();
+ } catch (IOException e) {
+ // Failed to send an opening handshake request to the server.
+ throw new WebSocketException(WebSocketError.OPENING_HAHDSHAKE_REQUEST_FAILURE, "Failed to send an opening handshake request to the server: " + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * Receive an opening handshake response from the WebSocket server.
+ */
+ private Map> readHandshake(WebSocketInputStream input, String key) throws WebSocketException {
+ return new HandshakeReader(this).readHandshake(input, key);
+ }
+
+
+ /**
+ * Start both the reading thread and the writing thread.
+ *
+ *
+ * The reading thread will call {@link #onReadingThreadStarted()}
+ * as its first step. Likewise, the writing thread will call
+ * {@link #onWritingThreadStarted()} as its first step. After
+ * both the threads have started, {@link #onThreadsStarted()} is
+ * called.
+ *
+ */
+ private void startThreads() {
+ ReadingThread readingThread = new ReadingThread(this);
+ WritingThread writingThread = new WritingThread(this);
+
+ synchronized (mThreadsLock) {
+ mReadingThread = readingThread;
+ mWritingThread = writingThread;
+ }
+
+ // Execute onThreadCreated of the listeners.
+ readingThread.callOnThreadCreated();
+ writingThread.callOnThreadCreated();
+
+ readingThread.start();
+ writingThread.start();
+ }
+
+
+ /**
+ * Stop both the reading thread and the writing thread.
+ *
+ *
+ * The reading thread will call {@link #onReadingThreadFinished(WebSocketFrame)}
+ * as its last step. Likewise, the writing thread will call {@link
+ * #onWritingThreadFinished(WebSocketFrame)} as its last step.
+ * After both the threads have stopped, {@link #onThreadsFinished()}
+ * is called.
+ *
+ */
+ private void stopThreads(long closeDelay) {
+ ReadingThread readingThread;
+ WritingThread writingThread;
+
+ synchronized (mThreadsLock) {
+ readingThread = mReadingThread;
+ writingThread = mWritingThread;
+
+ mReadingThread = null;
+ mWritingThread = null;
+ }
+
+ if (readingThread != null) {
+ readingThread.requestStop(closeDelay);
+ }
+
+ if (writingThread != null) {
+ writingThread.requestStop();
+ }
+ }
+
+
+ /**
+ * Get the input stream of the WebSocket connection.
+ */
+ WebSocketInputStream getInput() {
+ return mInput;
+ }
+
+
+ /**
+ * Get the output stream of the WebSocket connection.
+ */
+ WebSocketOutputStream getOutput() {
+ return mOutput;
+ }
+
+
+ /**
+ * Get the manager that manages the state of this {@code WebSocket} instance.
+ */
+ StateManager getStateManager() {
+ return mStateManager;
+ }
+
+
+ /**
+ * Get the manager that manages registered listeners.
+ */
+ ListenerManager getListenerManager() {
+ return mListenerManager;
+ }
+
+
+ /**
+ * Get the handshake builder. {@link HandshakeReader} uses this method.
+ */
+ HandshakeBuilder getHandshakeBuilder() {
+ return mHandshakeBuilder;
+ }
+
+
+ /**
+ * Set the agreed extensions. {@link HandshakeReader} uses this method.
+ */
+ void setAgreedExtensions(List extensions) {
+ mAgreedExtensions = extensions;
+ }
+
+
+ /**
+ * Set the agreed protocol. {@link HandshakeReader} uses this method.
+ */
+ void setAgreedProtocol(String protocol) {
+ mAgreedProtocol = protocol;
+ }
+
+
+ /**
+ * Called by the reading thread as its first step.
+ */
+ void onReadingThreadStarted() {
+ boolean bothStarted = false;
+
+ synchronized (mThreadsLock) {
+ mReadingThreadStarted = true;
+
+ if (mWritingThreadStarted) {
+ // Both the reading thread and the writing thread have started.
+ bothStarted = true;
+ }
+ }
+
+ // Call onConnected() method of listeners if not called yet.
+ callOnConnectedIfNotYet();
+
+ // If both the reading thread and the writing thread have started.
+ if (bothStarted) {
+ onThreadsStarted();
+ }
+ }
+
+
+ /**
+ * Called by the writing thread as its first step.
+ */
+ void onWritingThreadStarted() {
+ boolean bothStarted = false;
+
+ synchronized (mThreadsLock) {
+ mWritingThreadStarted = true;
+
+ if (mReadingThreadStarted) {
+ // Both the reading thread and the writing thread have started.
+ bothStarted = true;
+ }
+ }
+
+ // Call onConnected() method of listeners if not called yet.
+ callOnConnectedIfNotYet();
+
+ // If both the reading thread and the writing thread have started.
+ if (bothStarted) {
+ onThreadsStarted();
+ }
+ }
+
+
+ /**
+ * Call {@link WebSocketListener#onConnected(WebSocket, Map)} method
+ * of the registered listeners if it has not been called yet. Either
+ * the reading thread or the writing thread calls this method.
+ */
+ private void callOnConnectedIfNotYet() {
+ synchronized (mOnConnectedCalledLock) {
+ // If onConnected() has already been called.
+ if (mOnConnectedCalled) {
+ // Do not call onConnected() twice.
+ return;
+ }
+
+ mOnConnectedCalled = true;
+ }
+
+ // Notify the listeners that the handshake succeeded.
+ mListenerManager.callOnConnected(mServerHeaders);
+ }
+
+
+ /**
+ * Called when both the reading thread and the writing thread have started.
+ * This method is called in the context of either the reading thread or
+ * the writing thread.
+ */
+ private void onThreadsStarted() {
+ // Start sending ping frames periodically.
+ // If the interval is zero, this call does nothing.
+ mPingSender.start();
+
+ // Likewise, start the pong sender.
+ mPongSender.start();
+ }
+
+
+ /**
+ * Called by the reading thread as its last step.
+ */
+ void onReadingThreadFinished(WebSocketFrame closeFrame) {
+ synchronized (mThreadsLock) {
+ mReadingThreadFinished = true;
+ mServerCloseFrame = closeFrame;
+
+ if (mWritingThreadFinished == false) {
+ // Wait for the writing thread to finish.
+ return;
+ }
+ }
+
+ // Both the reading thread and the writing thread have finished.
+ onThreadsFinished();
+ }
+
+
+ /**
+ * Called by the writing thread as its last step.
+ */
+ void onWritingThreadFinished(WebSocketFrame closeFrame) {
+ synchronized (mThreadsLock) {
+ mWritingThreadFinished = true;
+ mClientCloseFrame = closeFrame;
+
+ if (mReadingThreadFinished == false) {
+ // Wait for the reading thread to finish.
+ return;
+ }
+ }
+
+ // Both the reading thread and the writing thread have finished.
+ onThreadsFinished();
+ }
+
+
+ /**
+ * Called when both the reading thread and the writing thread have finished.
+ * This method is called in the context of either the reading thread or
+ * the writing thread.
+ */
+ private void onThreadsFinished() {
+ finish();
+ }
+
+
+ void finish() {
+ // Stop the ping sender and the pong sender.
+ mPingSender.stop();
+ mPongSender.stop();
+
+ try {
+ // Close the raw socket.
+ mSocketConnector.getSocket().close();
+ } catch (Throwable t) {
+ // Ignore any error raised by close().
+ }
+
+ synchronized (mStateManager) {
+ // Change the state to CLOSED.
+ mStateManager.setState(CLOSED);
+ }
+
+ // Notify the listeners of the state change.
+ mListenerManager.callOnStateChanged(CLOSED);
+
+ // Notify the listeners that the WebSocket was disconnected.
+ mListenerManager.callOnDisconnected(mServerCloseFrame, mClientCloseFrame, mStateManager.getClosedByServer());
+ }
+
+
+ /**
+ * Call {@link #finish()} from within a separate thread.
+ */
+ private void finishAsynchronously() {
+ WebSocketThread thread = new FinishThread(this);
+
+ // Execute onThreadCreated() of the listeners.
+ thread.callOnThreadCreated();
+
+ thread.start();
+ }
+
+
+ /**
+ * Find a per-message compression extension from among the agreed extensions.
+ */
+ private PerMessageCompressionExtension findAgreedPerMessageCompressionExtension() {
+ if (mAgreedExtensions == null) {
+ return null;
+ }
+
+ for (WebSocketExtension extension : mAgreedExtensions) {
+ if (extension instanceof PerMessageCompressionExtension) {
+ return (PerMessageCompressionExtension) extension;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Get the PerMessageCompressionExtension in the agreed extensions.
+ * This method returns null if a per-message compression extension
+ * is not found in the agreed extensions.
+ */
+ PerMessageCompressionExtension getPerMessageCompressionExtension() {
+ return mPerMessageCompressionExtension;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketAdapter.java b/src/main/java/com/neovisionaries/ws/client/WebSocketAdapter.java
new file mode 100644
index 0000000..8a38029
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketAdapter.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * An empty implementation of {@link WebSocketListener} interface.
+ *
+ * @see WebSocketListener
+ */
+public class WebSocketAdapter implements WebSocketListener {
+ @Override
+ public void onStateChanged(WebSocket websocket, WebSocketState newState) throws Exception {
+ }
+
+
+ @Override
+ public void onConnected(WebSocket websocket, Map> headers) throws Exception {
+ }
+
+
+ @Override
+ public void onConnectError(WebSocket websocket, WebSocketException exception) throws Exception {
+ }
+
+
+ @Override
+ public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception {
+ }
+
+
+ @Override
+ public void onFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onContinuationFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onBinaryFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onCloseFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onPingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onPongFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onTextMessage(WebSocket websocket, String text) throws Exception {
+ }
+
+
+ @Override
+ public void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception {
+ }
+
+
+ @Override
+ public void onSendingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onFrameUnsent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onError(WebSocket websocket, WebSocketException cause) throws Exception {
+ }
+
+
+ @Override
+ public void onFrameError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onMessageError(WebSocket websocket, WebSocketException cause, List frames) throws Exception {
+ }
+
+
+ @Override
+ public void onMessageDecompressionError(WebSocket websocket, WebSocketException cause, byte[] compressed) throws Exception {
+ }
+
+
+ @Override
+ public void onTextMessageError(WebSocket websocket, WebSocketException cause, byte[] data) throws Exception {
+ }
+
+
+ @Override
+ public void onSendError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws Exception {
+ }
+
+
+ @Override
+ public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws Exception {
+ }
+
+
+ @Override
+ public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception {
+ }
+
+
+ @Override
+ public void onSendingHandshake(WebSocket websocket, String requestLine, List headers) throws Exception {
+ }
+
+
+ @Override
+ public void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception {
+ }
+
+
+ @Override
+ public void onThreadStarted(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception {
+ }
+
+
+ @Override
+ public void onThreadStopping(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception {
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketCloseCode.java b/src/main/java/com/neovisionaries/ws/client/WebSocketCloseCode.java
new file mode 100644
index 0000000..f9ce72c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketCloseCode.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Close code.
+ *
+ * @see RFC 6455, 7.4.1. Defined Status Codes
+ */
+public class WebSocketCloseCode {
+ /**
+ * 1000;
+ *
+ * 1000 indicates a normal closure, meaning that the purpose for
+ * which the connection was established has been fulfilled.
+ *
+ */
+ public static final int NORMAL = 1000;
+
+
+ /**
+ * 1001;
+ *
+ * 1001 indicates that an endpoint is "going away", such as a server
+ * going down or a browser having navigated away from a page.
+ *
+ */
+ public static final int AWAY = 1001;
+
+
+ /**
+ * 1002;
+ *
+ * 1002 indicates that an endpoint is terminating the connection due
+ * to a protocol error.
+ *
+ */
+ public static final int UNCONFORMED = 1002;
+
+
+ /**
+ * 1003;
+ *
+ * 1003 indicates that an endpoint is terminating the connection
+ * because it has received a type of data it cannot accept
+ * (e.g., an endpoint that understands only text data MAY
+ * send this if it receives a binary message).
+ *
+ */
+ public static final int UNACCEPTABLE = 1003;
+
+
+ /**
+ * 1005;
+ *
+ * 1005 is a reserved value and MUST NOT be set as a status code in a
+ * Close control frame by an endpoint. It is designated for use in
+ * applications expecting a status code to indicate that no status
+ * code was actually present.
+ *
+ */
+ public static final int NONE = 1005;
+
+
+ /**
+ * 1006;
+ *
+ * 1006 is a reserved value and MUST NOT be set as a status code in a
+ * Close control frame by an endpoint. It is designated for use in
+ * applications expecting a status code to indicate that the
+ * connection was closed abnormally, e.g., without sending or
+ * receiving a Close control frame.
+ *
+ */
+ public static final int ABNORMAL = 1006;
+
+
+ /**
+ * 1007;
+ *
+ * 1007 indicates that an endpoint is terminating the connection
+ * because it has received data within a message that was not
+ * consistent with the type of the message (e.g., non-UTF-8
+ * [RFC3629] data
+ * within a text message).
+ *
+ */
+ public static final int INCONSISTENT = 1007;
+
+
+ /**
+ * 1008;
+ *
+ * 1008 indicates that an endpoint is terminating the connection
+ * because it has received a message that violates its policy.
+ * This is a generic status code that can be returned when there
+ * is no other more suitable status code (e.g., 1003 or 1009)
+ * or if there is a need to hide specific details about the policy.
+ *
+ */
+ public static final int VIOLATED = 1008;
+
+
+ /**
+ * 1009;
+ *
+ * 1009 indicates that an endpoint is terminating the connection
+ * because it has received a message that is too big for it to
+ * process.
+ *
+ */
+ public static final int OVERSIZE = 1009;
+
+
+ /**
+ * 1010;
+ *
+ * 1010 indicates that an endpoint (client) is terminating the
+ * connection because it has expected the server to negotiate
+ * one or more extension, but the server didn't return them in
+ * the response message of the WebSocket handshake. The
+ * list of extensions that are needed SHOULD appear in the
+ * /reason/ part of the Close frame. Note that this status
+ * code is not used by the server, because it can fail the
+ * WebSocket handshake instead.
+ *
+ */
+ public static final int UNEXTENDED = 1010;
+
+
+ /**
+ * 1011;
+ *
+ * 1011 indicates that a server is terminating the connection because
+ * it encountered an unexpected condition that prevented it from
+ * fulfilling the request.
+ *
+ */
+ public static final int UNEXPECTED = 1011;
+
+
+ /**
+ * 1015;
+ *
+ * 1015 is a reserved value and MUST NOT be set as a status code in a
+ * Close control frame by an endpoint. It is designated for use in
+ * applications expecting a status code to indicate that the
+ * connection was closed due to a failure to perform a TLS handshake
+ * (e.g., the server certificate can't be verified).
+ *
+ */
+ public static final int INSECURE = 1015;
+
+
+ private WebSocketCloseCode() {
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketError.java b/src/main/java/com/neovisionaries/ws/client/WebSocketError.java
new file mode 100644
index 0000000..8fd51e5
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketError.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * WebSocket error codes.
+ *
+ * @see WebSocketException#getError()
+ */
+public enum WebSocketError {
+ /**
+ * The current state of the WebSocket is not CREATED.
+ *
+ *
+ * This error occurs if {@link WebSocket#connect()} is called
+ * when the state of the WebSocket is not {@link
+ * WebSocketState#CREATED CREATED}.
+ *
+ */
+ NOT_IN_CREATED_STATE,
+
+
+ /**
+ * Failed to get the input stream of the raw socket.
+ */
+ SOCKET_INPUT_STREAM_FAILURE,
+
+
+ /**
+ * Failed to get the output stream of the raw socket.
+ */
+ SOCKET_OUTPUT_STREAM_FAILURE,
+
+
+ /**
+ * Failed to send an opening handshake request to the server.
+ */
+ OPENING_HAHDSHAKE_REQUEST_FAILURE,
+
+
+ /**
+ * Failed to read an opening handshake response from the server.
+ */
+ OPENING_HANDSHAKE_RESPONSE_FAILURE,
+
+
+ /**
+ * The status line of the opening handshake response is empty.
+ */
+ STATUS_LINE_EMPTY,
+
+
+ /**
+ * The status line of the opening handshake response is badly formatted.
+ */
+ STATUS_LINE_BAD_FORMAT,
+
+
+ /**
+ * The status code of the opening handshake response is not {@code 101 Switching Protocols}.
+ */
+ NOT_SWITCHING_PROTOCOLS,
+
+
+ /**
+ * An error occurred while HTTP header section was being read.
+ */
+ HTTP_HEADER_FAILURE,
+
+
+ /**
+ * The opening handshake response does not contain {@code Upgrade} header.
+ */
+ NO_UPGRADE_HEADER,
+
+
+ /**
+ * {@code websocket} was not found in {@code Upgrade} header.
+ */
+ NO_WEBSOCKET_IN_UPGRADE_HEADER,
+
+
+ /**
+ * The opening handshake response does not contain {@code Connection} header.
+ */
+ NO_CONNECTION_HEADER,
+
+
+ /**
+ * {@code Upgrade} was not found in {@code Connection} header.
+ */
+ NO_UPGRADE_IN_CONNECTION_HEADER,
+
+
+ /**
+ * The opening handshake response does not contain {@code Sec-WebSocket-Accept} header.
+ */
+ NO_SEC_WEBSOCKET_ACCEPT_HEADER,
+
+
+ /**
+ * The value of {@code Sec-WebSocket-Accept} header is different from the expected one.
+ */
+ UNEXPECTED_SEC_WEBSOCKET_ACCEPT_HEADER,
+
+
+ /**
+ * The value in {@code Sec-WebSocket-Extensions} failed to be parsed.
+ */
+ EXTENSION_PARSE_ERROR,
+
+
+ /**
+ * The extension contained in {@code Sec-WebSocket-Extensions} header is not supported.
+ */
+ UNSUPPORTED_EXTENSION,
+
+
+ /**
+ * The combination of the extensions contained in {@code Sec-WebSocket-Extensions} header
+ * causes conflicts.
+ *
+ * @since 1.15
+ */
+ EXTENSIONS_CONFLICT,
+
+
+ /**
+ * The protocol contained in {@code Sec-WebSocket-Protocol} header is not supported.
+ */
+ UNSUPPORTED_PROTOCOL,
+
+
+ /**
+ * The end of the stream has been reached unexpectedly.
+ */
+ INSUFFICENT_DATA,
+
+
+ /**
+ * The payload length of a frame is invalid.
+ */
+ INVALID_PAYLOAD_LENGTH,
+
+
+ /**
+ * The payload length of a frame exceeds the maximum array size in Java.
+ */
+ TOO_LONG_PAYLOAD,
+
+
+ /**
+ * {@link OutOfMemoryError} occurred during a trial to allocate a memory area for a frame's payload.
+ */
+ INSUFFICIENT_MEMORY_FOR_PAYLOAD,
+
+
+ /**
+ * Interruption occurred while a frame was being read from the WebSocket.
+ */
+ INTERRUPTED_IN_READING,
+
+
+ /**
+ * An I/O error occurred while a frame was being read from the WebSocket.
+ */
+ IO_ERROR_IN_READING,
+
+
+ /**
+ * An I/O error occurred when a frame was tried to be sent.
+ */
+ IO_ERROR_IN_WRITING,
+
+
+ /**
+ * Flushing frames to the server failed.
+ */
+ FLUSH_ERROR,
+
+
+ /**
+ * At least one of the reserved bits of a frame is set.
+ *
+ *
+ * MUST be 0 unless an extension is negotiated that defines meanings
+ * for non-zero values. If a nonzero value is received and none of
+ * the negotiated extensions defines the meaning of such a nonzero
+ * value, the receiving endpoint MUST Fail the WebSocket Connection.
+ *
+ *
+ *
+ *
+ * By calling {@link WebSocket#setExtended(boolean) WebSocket.setExtended}{@code
+ * (true)}, you can skip the validity check of the RSV1/RSV2/RSV3 bits.
+ *
+ *
+ *
+ * This error code is not used in version 1.15 and after.
+ *
+ */
+ NON_ZERO_RESERVED_BITS,
+
+
+ /**
+ * A reserved bit of a frame has an unexpected value.
+ *
+ *
+ * MUST be 0 unless an extension is negotiated that defines meanings
+ * for non-zero values. If a nonzero value is received and none of
+ * the negotiated extensions defines the meaning of such a nonzero
+ * value, the receiving endpoint MUST Fail the WebSocket Connection.
+ *
+ *
+ *
+ *
+ * By calling {@link WebSocket#setExtended(boolean) WebSocket.setExtended}{@code
+ * (true)}, you can skip the validity check of the RSV1/RSV2/RSV3 bits.
+ *
+ *
+ * @since 1.15
+ */
+ UNEXPECTED_RESERVED_BIT,
+
+
+ /**
+ * A frame from the server is masked.
+ *
+ *
+ * Control frames (see Section 5.5) MAY be injected in the middle of
+ * a fragmented message. Control frames themselves MUST NOT be fragmented.
+ *
+ *
+ */
+ FRAGMENTED_CONTROL_FRAME,
+
+
+ /**
+ * A continuation frame was detected although a continuation had not started.
+ */
+ UNEXPECTED_CONTINUATION_FRAME,
+
+
+ /**
+ * A non-control frame was detected although the existing continuation had not been closed.
+ */
+ CONTINUATION_NOT_CLOSED,
+
+
+ /**
+ * The payload size of a control frame exceeds the maximum size (125 bytes).
+ *
+ *
+ *
+ * @since 1.15
+ */
+ PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS,
+
+
+ /**
+ * Compression failed.
+ *
+ * @since 1.17
+ */
+ COMPRESSION_ERROR,
+
+
+ /**
+ * Decompression failed.
+ *
+ * @since 1.16
+ */
+ DECOMPRESSION_ERROR,
+
+
+ /**
+ * {@link java.net.Socket#connect(java.net.SocketAddress, int)
+ * Socket.connect()} failed.
+ *
+ * @since 1.20
+ */
+ SOCKET_CONNECT_ERROR,
+
+
+ /**
+ * Handshake with a proxy server failed.
+ *
+ * @since 1.20
+ */
+ PROXY_HANDSHAKE_ERROR,
+
+
+ /**
+ * Failed to overlay an existing socket.
+ *
+ * @since 1.20
+ */
+ SOCKET_OVERLAY_ERROR,
+
+
+ /**
+ * SSL handshake with a WebSocket endpoint failed.
+ *
+ * @since 1.20
+ */
+ SSL_HANDSHAKE_ERROR,
+
+
+ /**
+ * No more frame can be read because the end of the input stream has been reached.
+ *
+ *
+ * This happens when the WebSocket connection is closed without receiving a
+ * close frame
+ * from the WebSocket server. Strictly speaking, it is a violation against
+ * RFC 6455, but it seems some
+ * server implementations sometimes close a connection without sending a close
+ * frame.
+ *
+ *
+ * @since 1.29
+ */
+ NO_MORE_FRAME,
+
+
+ /**
+ * The certificate of the peer does not match the expected hostname.
+ *
+ *
+ * When {@link WebSocketException#getError()} returns this error code, the
+ * {@link WebSocketException} can be cast to {@link HostnameUnverifiedException}
+ * through which you can get the
+ *
+ *
+ * @since 2.1
+ */
+ HOSTNAME_UNVERIFIED,;
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketException.java b/src/main/java/com/neovisionaries/ws/client/WebSocketException.java
new file mode 100644
index 0000000..1d8bc89
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketException.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * WebSocket exception.
+ */
+public class WebSocketException extends Exception {
+ private static final long serialVersionUID = 1L;
+ private final WebSocketError mError;
+
+
+ public WebSocketException(WebSocketError error) {
+ mError = error;
+ }
+
+
+ public WebSocketException(WebSocketError error, String message) {
+ super(message);
+
+ mError = error;
+ }
+
+
+ public WebSocketException(WebSocketError error, Throwable cause) {
+ super(cause);
+
+ mError = error;
+ }
+
+
+ public WebSocketException(WebSocketError error, String message, Throwable cause) {
+ super(message, cause);
+
+ mError = error;
+ }
+
+
+ public WebSocketError getError() {
+ return mError;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketExtension.java b/src/main/java/com/neovisionaries/ws/client/WebSocketExtension.java
new file mode 100644
index 0000000..4a45046
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketExtension.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+
+/**
+ * A class to hold the name and the parameters of
+ * a WebSocket extension.
+ */
+public class WebSocketExtension {
+ /**
+ * The name of permessage-deflate extension that is
+ * defined in 7. The "permessage-deflate" Extension in RFC 7692.
+ *
+ * @since 1.17
+ */
+ public static final String PERMESSAGE_DEFLATE = "permessage-deflate";
+
+
+ private final String mName;
+ private final Map mParameters;
+
+
+ /**
+ * Constructor with an extension name.
+ *
+ * @param name
+ * The extension name.
+ *
+ * @throws IllegalArgumentException
+ * The given name is not a valid token.
+ */
+ public WebSocketExtension(String name) {
+ // Check the validity of the name.
+ if (Token.isValid(name) == false) {
+ // The name is not a valid token.
+ throw new IllegalArgumentException("'name' is not a valid token.");
+ }
+
+ mName = name;
+ mParameters = new LinkedHashMap();
+ }
+
+
+ /**
+ * Copy constructor.
+ *
+ * @param source
+ * A source extension. Must not be {@code null}.
+ *
+ * @throws IllegalArgumentException
+ * The given argument is {@code null}.
+ *
+ * @since 1.6
+ */
+ public WebSocketExtension(WebSocketExtension source) {
+ if (source == null) {
+ // If the given instance is null.
+ throw new IllegalArgumentException("'source' is null.");
+ }
+
+ mName = source.getName();
+ mParameters = new LinkedHashMap(source.getParameters());
+ }
+
+
+ /**
+ * Get the extension name.
+ *
+ * @return
+ * The extension name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+
+ /**
+ * Get the parameters.
+ *
+ * @return
+ * The parameters.
+ */
+ public Map getParameters() {
+ return mParameters;
+ }
+
+
+ /**
+ * Check if the parameter identified by the key is contained.
+ *
+ * @param key
+ * The name of the parameter.
+ *
+ * @return
+ * {@code true} if the parameter is contained.
+ */
+ public boolean containsParameter(String key) {
+ return mParameters.containsKey(key);
+ }
+
+
+ /**
+ * Get the value of the specified parameter.
+ *
+ * @param key
+ * The name of the parameter.
+ *
+ * @return
+ * The value of the parameter. {@code null} may be returned.
+ */
+ public String getParameter(String key) {
+ return mParameters.get(key);
+ }
+
+
+ /**
+ * Set a value to the specified parameter.
+ *
+ * @param key
+ * The name of the parameter.
+ *
+ * @param value
+ * The value of the parameter. If not {@code null}, it must be
+ * a valid token. Note that RFC 6455 says "When using the quoted-string syntax
+ * variant, the value after quoted-string unescaping MUST
+ * conform to the 'token' ABNF."
+ *
+ * @return
+ * {@code this} object.
+ *
+ * @throws IllegalArgumentException
+ *
+ *
The key is not a valid token.
+ *
The value is not {@code null} and it is not a valid token.
+ *
+ */
+ public WebSocketExtension setParameter(String key, String value) {
+ // Check the validity of the key.
+ if (Token.isValid(key) == false) {
+ // The key is not a valid token.
+ throw new IllegalArgumentException("'key' is not a valid token.");
+ }
+
+ // If the value is not null.
+ if (value != null) {
+ // Check the validity of the value.
+ if (Token.isValid(value) == false) {
+ // The value is not a valid token.
+ throw new IllegalArgumentException("'value' is not a valid token.");
+ }
+ }
+
+ mParameters.put(key, value);
+
+ return this;
+ }
+
+
+ /**
+ * Stringify this object into the format "{name}[; {key}[={value}]]*".
+ */
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(mName);
+
+ for (Map.Entry entry : mParameters.entrySet()) {
+ // "; {key}"
+ builder.append("; ").append(entry.getKey());
+
+ String value = entry.getValue();
+
+ if (value != null && value.length() != 0) {
+ // "={value}"
+ builder.append("=").append(value);
+ }
+ }
+
+ return builder.toString();
+ }
+
+
+ /**
+ * Validate this instance. This method is expected to be overridden.
+ */
+ void validate() throws WebSocketException {
+ }
+
+
+ /**
+ * Parse a string as a {@link WebSocketExtension}. The input string
+ * should comply with the format described in 9.1. Negotiating
+ * Extensions in RFC 6455.
+ *
+ * @param string
+ * A string that represents a WebSocket extension.
+ *
+ * @return
+ * A new {@link WebSocketExtension} instance that represents
+ * the given string. If the input string does not comply with
+ * RFC 6455, {@code null} is returned.
+ */
+ public static WebSocketExtension parse(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ // Split the string by semi-colons.
+ String[] elements = string.trim().split("\\s*;\\s*");
+
+ if (elements.length == 0) {
+ // Even an extension name is not included.
+ return null;
+ }
+
+ // The first element is the extension name.
+ String name = elements[0];
+
+ if (Token.isValid(name) == false) {
+ // The extension name is not a valid token.
+ return null;
+ }
+
+ // Create an instance for the extension name.
+ WebSocketExtension extension = createInstance(name);
+
+ // For each "{key}[={value}]".
+ for (int i = 1; i < elements.length; ++i) {
+ // Split by '=' to get the key and the value.
+ String[] pair = elements[i].split("\\s*=\\s*", 2);
+
+ // If {key} is not contained.
+ if (pair.length == 0 || pair[0].length() == 0) {
+ // Ignore.
+ continue;
+ }
+
+ // The name of the parameter.
+ String key = pair[0];
+
+ if (Token.isValid(key) == false) {
+ // The parameter name is not a valid token.
+ // Ignore this parameter.
+ continue;
+ }
+
+ // The value of the parameter.
+ String value = extractValue(pair);
+
+ if (value != null) {
+ if (Token.isValid(value) == false) {
+ // The parameter value is not a valid token.
+ // Ignore this parameter.
+ continue;
+ }
+ }
+
+ // Add the pair of the key and the value.
+ extension.setParameter(key, value);
+ }
+
+ return extension;
+ }
+
+
+ private static String extractValue(String[] pair) {
+ if (pair.length != 2) {
+ return null;
+ }
+
+ return Token.unquote(pair[1]);
+ }
+
+
+ private static WebSocketExtension createInstance(String name) {
+ if (PERMESSAGE_DEFLATE.equals(name)) {
+ return new PerMessageDeflateExtension(name);
+ }
+
+ return new WebSocketExtension(name);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketFactory.java b/src/main/java/com/neovisionaries/ws/client/WebSocketFactory.java
new file mode 100644
index 0000000..10058c1
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketFactory.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+
+/**
+ * Factory to create {@link WebSocket} instances.
+ */
+public class WebSocketFactory {
+ private final SocketFactorySettings mSocketFactorySettings;
+ private final ProxySettings mProxySettings;
+ private int mConnectionTimeout;
+
+
+ public WebSocketFactory() {
+ mSocketFactorySettings = new SocketFactorySettings();
+ mProxySettings = new ProxySettings(this);
+ }
+
+
+ /**
+ * Get the socket factory that has been set by {@link
+ * #setSocketFactory(SocketFactory)}.
+ *
+ * @return
+ * The socket factory.
+ */
+ public SocketFactory getSocketFactory() {
+ return mSocketFactorySettings.getSocketFactory();
+ }
+
+
+ /**
+ * Set a socket factory.
+ * See {@link #createSocket(URI)} for details.
+ *
+ * @param factory
+ * A socket factory.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public WebSocketFactory setSocketFactory(SocketFactory factory) {
+ mSocketFactorySettings.setSocketFactory(factory);
+
+ return this;
+ }
+
+
+ /**
+ * Get the SSL socket factory that has been set by {@link
+ * #setSSLSocketFactory(SSLSocketFactory)}.
+ *
+ * @return
+ * The SSL socket factory.
+ */
+ public SSLSocketFactory getSSLSocketFactory() {
+ return mSocketFactorySettings.getSSLSocketFactory();
+ }
+
+
+ /**
+ * Set an SSL socket factory.
+ * See {@link #createSocket(URI)} for details.
+ *
+ * @param factory
+ * An SSL socket factory.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public WebSocketFactory setSSLSocketFactory(SSLSocketFactory factory) {
+ mSocketFactorySettings.setSSLSocketFactory(factory);
+
+ return this;
+ }
+
+
+ /**
+ * Get the SSL context that has been set by {@link #setSSLContext(SSLContext)}.
+ *
+ * @return
+ * The SSL context.
+ */
+ public SSLContext getSSLContext() {
+ return mSocketFactorySettings.getSSLContext();
+ }
+
+
+ /**
+ * Set an SSL context to get a socket factory.
+ * See {@link #createSocket(URI)} for details.
+ *
+ * @param context
+ * An SSL context.
+ *
+ * @return
+ * {@code this} instance.
+ */
+ public WebSocketFactory setSSLContext(SSLContext context) {
+ mSocketFactorySettings.setSSLContext(context);
+
+ return this;
+ }
+
+
+ /**
+ * Get the proxy settings.
+ *
+ * @return
+ * The proxy settings.
+ *
+ * @since 1.3
+ *
+ * @see ProxySettings
+ */
+ public ProxySettings getProxySettings() {
+ return mProxySettings;
+ }
+
+
+ /**
+ * Get the timeout value in milliseconds for socket connection.
+ * The default value is 0 and it means an infinite timeout.
+ *
+ *
+ * When a {@code createSocket} method which does not have {@code
+ * timeout} argument is called, the value returned by this method
+ * is used as a timeout value for socket connection.
+ *
+ *
+ * @return
+ * The connection timeout value in milliseconds.
+ *
+ * @since 1.10
+ */
+ public int getConnectionTimeout() {
+ return mConnectionTimeout;
+ }
+
+
+ /**
+ * Set the timeout value in milliseconds for socket connection.
+ * A timeout of zero is interpreted as an infinite timeout.
+ *
+ * @param timeout
+ * The connection timeout value in milliseconds.
+ *
+ * @return
+ * {@code this} object.
+ *
+ * @throws IllegalArgumentException
+ * The given timeout value is negative.
+ *
+ * @since 1.10
+ */
+ public WebSocketFactory setConnectionTimeout(int timeout) {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout value cannot be negative.");
+ }
+
+ mConnectionTimeout = timeout;
+
+ return this;
+ }
+
+
+ /**
+ * Create a WebSocket.
+ *
+ *
+ * This method is an alias of {@link #createSocket(String, int)
+ * createSocket}{@code (uri, }{@link #getConnectionTimeout()}{@code )}.
+ *
+ *
+ * @param uri
+ * The URI of the WebSocket endpoint on the server side.
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URI is {@code null} or violates RFC 2396.
+ *
+ * @throws IOException
+ * Failed to create a socket. Or, HTTP proxy handshake or SSL
+ * handshake failed.
+ */
+ public WebSocket createSocket(String uri) throws IOException {
+ return createSocket(uri, getConnectionTimeout());
+ }
+
+
+ /**
+ * Create a WebSocket.
+ *
+ *
+ * This method is an alias of {@link #createSocket(URI, int) createSocket}{@code
+ * (}{@link URI#create(String) URI.create}{@code (uri), timeout)}.
+ *
+ *
+ * @param uri
+ * The URI of the WebSocket endpoint on the server side.
+ *
+ * @param timeout
+ * The timeout value in milliseconds for socket connection.
+ * A timeout of zero is interpreted as an infinite timeout.
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URI is {@code null} or violates RFC 2396, or
+ * the given timeout value is negative.
+ *
+ * @throws IOException
+ * Failed to create a socket. Or, HTTP proxy handshake or SSL
+ * handshake failed.
+ *
+ * @since 1.10
+ */
+ public WebSocket createSocket(String uri, int timeout) throws IOException {
+ if (uri == null) {
+ throw new IllegalArgumentException("The given URI is null.");
+ }
+
+ if (timeout < 0) {
+ throw new IllegalArgumentException("The given timeout value is negative.");
+ }
+
+ return createSocket(URI.create(uri), timeout);
+ }
+
+
+ /**
+ * Create a WebSocket.
+ *
+ *
+ * This method is an alias of {@link #createSocket(URL, int) createSocket}{@code
+ * (url, }{@link #getConnectionTimeout()}{@code )}.
+ *
+ *
+ * @param url
+ * The URL of the WebSocket endpoint on the server side.
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URL is {@code null} or failed to be converted into a URI.
+ *
+ * @throws IOException
+ * Failed to create a socket. Or, HTTP proxy handshake or SSL
+ * handshake failed.
+ */
+ public WebSocket createSocket(URL url) throws IOException {
+ return createSocket(url, getConnectionTimeout());
+ }
+
+
+ /**
+ * Create a WebSocket.
+ *
+ *
+ * This method is an alias of {@link #createSocket(URI, int) createSocket}{@code
+ * (url.}{@link URL#toURI() toURI()}{@code , timeout)}.
+ *
+ *
+ * @param url
+ * The URL of the WebSocket endpoint on the server side.
+ *
+ * @param timeout
+ * The timeout value in milliseconds for socket connection.
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URL is {@code null} or failed to be converted into a URI,
+ * or the given timeout value is negative.
+ *
+ * @throws IOException
+ * Failed to create a socket. Or, HTTP proxy handshake or SSL
+ * handshake failed.
+ *
+ * @since 1.10
+ */
+ public WebSocket createSocket(URL url, int timeout) throws IOException {
+ if (url == null) {
+ throw new IllegalArgumentException("The given URL is null.");
+ }
+
+ if (timeout < 0) {
+ throw new IllegalArgumentException("The given timeout value is negative.");
+ }
+
+ try {
+ return createSocket(url.toURI(), timeout);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Failed to convert the given URL into a URI.");
+ }
+ }
+
+
+ /**
+ * Create a WebSocket. This method is an alias of {@link #createSocket(URI, int)
+ * createSocket}{@code (uri, }{@link #getConnectionTimeout()}{@code )}.
+ *
+ *
+ * A socket factory (= a {@link SocketFactory} instance) to create a raw
+ * socket (= a {@link Socket} instance) is determined as described below.
+ *
+ *
+ *
+ *
+ * If the scheme of the URI is either {@code wss} or {@code https},
+ *
+ *
+ * If an {@link SSLContext} instance has been set by {@link
+ * #setSSLContext(SSLContext)}, the value returned from {@link
+ * SSLContext#getSocketFactory()} method of the instance is used.
+ *
+ * Otherwise, if an {@link SSLSocketFactory} instance has been
+ * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the
+ * instance is used.
+ *
+ * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()}
+ * is used.
+ *
+ *
+ * Otherwise (= the scheme of the URI is either {@code ws} or {@code http}),
+ *
+ *
+ * If a {@link SocketFactory} instance has been set by {@link
+ * #setSocketFactory(SocketFactory)}, the instance is used.
+ *
+ * Otherwise, the value returned from {@link SocketFactory#getDefault()}
+ * is used.
+ *
+ *
+ *
+ * @param uri
+ * The URI of the WebSocket endpoint on the server side.
+ * The scheme part of the URI must be one of {@code ws},
+ * {@code wss}, {@code http} and {@code https}
+ * (case-insensitive).
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URI is {@code null} or violates RFC 2396.
+ *
+ * @throws IOException
+ * Failed to create a socket.
+ */
+ public WebSocket createSocket(URI uri) throws IOException {
+ return createSocket(uri, getConnectionTimeout());
+ }
+
+
+ /**
+ * Create a WebSocket.
+ *
+ *
+ * A socket factory (= a {@link SocketFactory} instance) to create a raw
+ * socket (= a {@link Socket} instance) is determined as described below.
+ *
+ *
+ *
+ *
+ * If the scheme of the URI is either {@code wss} or {@code https},
+ *
+ *
+ * If an {@link SSLContext} instance has been set by {@link
+ * #setSSLContext(SSLContext)}, the value returned from {@link
+ * SSLContext#getSocketFactory()} method of the instance is used.
+ *
+ * Otherwise, if an {@link SSLSocketFactory} instance has been
+ * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the
+ * instance is used.
+ *
+ * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()}
+ * is used.
+ *
+ *
+ * Otherwise (= the scheme of the URI is either {@code ws} or {@code http}),
+ *
+ *
+ * If a {@link SocketFactory} instance has been set by {@link
+ * #setSocketFactory(SocketFactory)}, the instance is used.
+ *
+ * Otherwise, the value returned from {@link SocketFactory#getDefault()}
+ * is used.
+ *
+ *
+ *
+ * @param uri
+ * The URI of the WebSocket endpoint on the server side.
+ * The scheme part of the URI must be one of {@code ws},
+ * {@code wss}, {@code http} and {@code https}
+ * (case-insensitive).
+ *
+ * @param timeout
+ * The timeout value in milliseconds for socket connection.
+ *
+ * @return
+ * A WebSocket.
+ *
+ * @throws IllegalArgumentException
+ * The given URI is {@code null} or violates RFC 2396, or
+ * the given timeout value is negative.
+ *
+ * @throws IOException
+ * Failed to create a socket.
+ *
+ * @since 1.10
+ */
+ public WebSocket createSocket(URI uri, int timeout) throws IOException {
+ if (uri == null) {
+ throw new IllegalArgumentException("The given URI is null.");
+ }
+
+ if (timeout < 0) {
+ throw new IllegalArgumentException("The given timeout value is negative.");
+ }
+
+ // Split the URI.
+ String scheme = uri.getScheme();
+ String userInfo = uri.getUserInfo();
+ String host = Misc.extractHost(uri);
+ int port = uri.getPort();
+ String path = uri.getRawPath();
+ String query = uri.getRawQuery();
+
+ return createSocket(scheme, userInfo, host, port, path, query, timeout);
+ }
+
+
+ private WebSocket createSocket(String scheme, String userInfo, String host, int port, String path, String query, int timeout) throws IOException {
+ // True if 'scheme' is 'wss' or 'https'.
+ boolean secure = isSecureConnectionRequired(scheme);
+
+ // Check if 'host' is specified.
+ if (host == null || host.length() == 0) {
+ throw new IllegalArgumentException("The host part is empty.");
+ }
+
+ // Determine the path.
+ path = determinePath(path);
+
+ // Create a Socket instance and a connector to connect to the server.
+ SocketConnector connector = createRawSocket(host, port, secure, timeout);
+
+ // Create a WebSocket instance.
+ return createWebSocket(secure, userInfo, host, port, path, query, connector);
+ }
+
+
+
+ private static boolean isSecureConnectionRequired(String scheme) {
+ if (scheme == null || scheme.length() == 0) {
+ throw new IllegalArgumentException("The scheme part is empty.");
+ }
+
+ if ("wss".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
+ return true;
+ }
+
+ if ("ws".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
+ return false;
+ }
+
+ throw new IllegalArgumentException("Bad scheme: " + scheme);
+ }
+
+
+ private static String determinePath(String path) {
+ if (path == null || path.length() == 0) {
+ return "/";
+ }
+
+ if (path.startsWith("/")) {
+ return path;
+ } else {
+ return "/" + path;
+ }
+ }
+
+
+ private SocketConnector createRawSocket(String host, int port, boolean secure, int timeout) throws IOException {
+ // Determine the port number. Especially, if 'port' is -1,
+ // it is converted to 80 or 443.
+ port = determinePort(port, secure);
+
+ // True if a proxy server should be used.
+ boolean proxied = (mProxySettings.getHost() != null);
+
+ // See "Figure 2 -- Proxy server traversal decision tree" at
+ // http://www.infoq.com/articles/Web-Sockets-Proxy-Servers
+
+ if (proxied) {
+ // Create a connector to connect to the proxy server.
+ return createProxiedRawSocket(host, port, secure, timeout);
+ } else {
+ // Create a connector to connect to the WebSocket endpoint directly.
+ return createDirectRawSocket(host, port, secure, timeout);
+ }
+ }
+
+
+ private SocketConnector createProxiedRawSocket(String host, int port, boolean secure, int timeout) throws IOException {
+ // Determine the port number of the proxy server.
+ // Especially, if getPort() returns -1, the value
+ // is converted to 80 or 443.
+ int proxyPort = determinePort(mProxySettings.getPort(), mProxySettings.isSecure());
+
+ // Select a socket factory.
+ SocketFactory socketFactory = mProxySettings.selectSocketFactory();
+
+ // Let the socket factory create a socket.
+ Socket socket = socketFactory.createSocket();
+
+ // The address to connect to.
+ Address address = new Address(mProxySettings.getHost(), proxyPort);
+
+ // The delegatee for the handshake with the proxy.
+ ProxyHandshaker handshaker = new ProxyHandshaker(socket, host, port, mProxySettings);
+
+ // SSLSocketFactory for SSL handshake with the WebSocket endpoint.
+ SSLSocketFactory sslSocketFactory = secure ? (SSLSocketFactory) mSocketFactorySettings.selectSocketFactory(secure) : null;
+
+ // Create an instance that will execute the task to connect to the server later.
+ return new SocketConnector(
+ socket, address, timeout, handshaker, sslSocketFactory, host, port);
+ }
+
+
+ private SocketConnector createDirectRawSocket(String host, int port, boolean secure, int timeout) throws IOException {
+ // Select a socket factory.
+ SocketFactory factory = mSocketFactorySettings.selectSocketFactory(secure);
+
+ // Let the socket factory create a socket.
+ Socket socket = factory.createSocket();
+
+ // The address to connect to.
+ Address address = new Address(host, port);
+
+ // Create an instance that will execute the task to connect to the server later.
+ return new SocketConnector(socket, address, timeout);
+ }
+
+
+ private static int determinePort(int port, boolean secure) {
+ if (0 <= port) {
+ return port;
+ }
+
+ if (secure) {
+ return 443;
+ } else {
+ return 80;
+ }
+ }
+
+
+ private WebSocket createWebSocket(boolean secure, String userInfo, String host, int port, String path, String query, SocketConnector connector) {
+ // The value for "Host" HTTP header.
+ if (0 <= port) {
+ host = host + ":" + port;
+ }
+
+ // The value for Request-URI of Request-Line.
+ if (query != null) {
+ path = path + "?" + query;
+ }
+
+ return new WebSocket(this, secure, userInfo, host, path, connector);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketFrame.java b/src/main/java/com/neovisionaries/ws/client/WebSocketFrame.java
new file mode 100644
index 0000000..14309a7
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketFrame.java
@@ -0,0 +1,1025 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.neovisionaries.ws.client.WebSocketOpcode.BINARY;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CLOSE;
+import static com.neovisionaries.ws.client.WebSocketOpcode.CONTINUATION;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PING;
+import static com.neovisionaries.ws.client.WebSocketOpcode.PONG;
+import static com.neovisionaries.ws.client.WebSocketOpcode.TEXT;
+
+
+/**
+ * WebSocket frame.
+ *
+ * @see RFC 6455, 5. Data Framing
+ */
+public class WebSocketFrame {
+ private boolean mFin;
+ private boolean mRsv1;
+ private boolean mRsv2;
+ private boolean mRsv3;
+ private int mOpcode;
+ private boolean mMask;
+ private byte[] mPayload;
+ private Object requestWrapper;
+
+ public Object getRequestWrapper() {
+ return requestWrapper;
+ }
+
+ public WebSocketFrame setRequestWrapper(Object requestWrapper) {
+ this.requestWrapper = requestWrapper;
+ return this;
+ }
+
+ /**
+ * Get the value of FIN bit.
+ *
+ * @return The value of FIN bit.
+ */
+ public boolean getFin() {
+ return mFin;
+ }
+
+
+ /**
+ * Set the value of FIN bit.
+ *
+ * @param fin The value of FIN bit.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setFin(boolean fin) {
+ mFin = fin;
+
+ return this;
+ }
+
+
+ /**
+ * Get the value of RSV1 bit.
+ *
+ * @return The value of RSV1 bit.
+ */
+ public boolean getRsv1() {
+ return mRsv1;
+ }
+
+
+ /**
+ * Set the value of RSV1 bit.
+ *
+ * @param rsv1 The value of RSV1 bit.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setRsv1(boolean rsv1) {
+ mRsv1 = rsv1;
+
+ return this;
+ }
+
+
+ /**
+ * Get the value of RSV2 bit.
+ *
+ * @return The value of RSV2 bit.
+ */
+ public boolean getRsv2() {
+ return mRsv2;
+ }
+
+
+ /**
+ * Set the value of RSV2 bit.
+ *
+ * @param rsv2 The value of RSV2 bit.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setRsv2(boolean rsv2) {
+ mRsv2 = rsv2;
+
+ return this;
+ }
+
+
+ /**
+ * Get the value of RSV3 bit.
+ *
+ * @return The value of RSV3 bit.
+ */
+ public boolean getRsv3() {
+ return mRsv3;
+ }
+
+
+ /**
+ * Set the value of RSV3 bit.
+ *
+ * @param rsv3 The value of RSV3 bit.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setRsv3(boolean rsv3) {
+ mRsv3 = rsv3;
+
+ return this;
+ }
+
+
+ /**
+ * Get the opcode.
+ *
+ *
+ *
WebSocket opcode
+ *
+ *
+ *
Value
+ *
Description
+ *
+ *
+ *
+ *
+ *
0x0
+ *
Frame continuation
+ *
+ *
+ *
0x1
+ *
Text frame
+ *
+ *
+ *
0x2
+ *
Binary frame
+ *
+ *
+ *
0x3-0x7
+ *
Reserved
+ *
+ *
+ *
0x8
+ *
Connection close
+ *
+ *
+ *
0x9
+ *
Ping
+ *
+ *
+ *
0xA
+ *
Pong
+ *
+ *
+ *
0xB-0xF
+ *
Reserved
+ *
+ *
+ *
+ *
+ * @return The opcode.
+ * @see WebSocketOpcode
+ */
+ public int getOpcode() {
+ return mOpcode;
+ }
+
+
+ /**
+ * Set the opcode
+ *
+ * @param opcode The opcode.
+ * @return {@code this} object.
+ * @see WebSocketOpcode
+ */
+ public WebSocketFrame setOpcode(int opcode) {
+ mOpcode = opcode;
+
+ return this;
+ }
+
+
+ /**
+ * Check if this frame is a continuation frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0x0 ({@link WebSocketOpcode#CONTINUATION}).
+ *
+ *
+ * @return {@code true} if this frame is a continuation frame
+ * (= if the opcode is 0x0).
+ */
+ public boolean isContinuationFrame() {
+ return (mOpcode == CONTINUATION);
+ }
+
+
+ /**
+ * Check if this frame is a text frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0x1 ({@link WebSocketOpcode#TEXT}).
+ *
+ *
+ * @return {@code true} if this frame is a text frame
+ * (= if the opcode is 0x1).
+ */
+ public boolean isTextFrame() {
+ return (mOpcode == TEXT);
+ }
+
+
+ /**
+ * Check if this frame is a binary frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0x2 ({@link WebSocketOpcode#BINARY}).
+ *
+ *
+ * @return {@code true} if this frame is a binary frame
+ * (= if the opcode is 0x2).
+ */
+ public boolean isBinaryFrame() {
+ return (mOpcode == BINARY);
+ }
+
+
+ /**
+ * Check if this frame is a close frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0x8 ({@link WebSocketOpcode#CLOSE}).
+ *
+ *
+ * @return {@code true} if this frame is a close frame
+ * (= if the opcode is 0x8).
+ */
+ public boolean isCloseFrame() {
+ return (mOpcode == CLOSE);
+ }
+
+
+ /**
+ * Check if this frame is a ping frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0x9 ({@link WebSocketOpcode#PING}).
+ *
+ *
+ * @return {@code true} if this frame is a ping frame
+ * (= if the opcode is 0x9).
+ */
+ public boolean isPingFrame() {
+ return (mOpcode == PING);
+ }
+
+
+ /**
+ * Check if this frame is a pong frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is 0xA ({@link WebSocketOpcode#PONG}).
+ *
+ *
+ * @return {@code true} if this frame is a pong frame
+ * (= if the opcode is 0xA).
+ */
+ public boolean isPongFrame() {
+ return (mOpcode == PONG);
+ }
+
+
+ /**
+ * Check if this frame is a data frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is in between 0x1 and 0x7.
+ *
+ *
+ * @return {@code true} if this frame is a data frame
+ * (= if the opcode is in between 0x1 and 0x7).
+ */
+ public boolean isDataFrame() {
+ return (0x1 <= mOpcode && mOpcode <= 0x7);
+ }
+
+
+ /**
+ * Check if this frame is a control frame.
+ *
+ *
+ * This method returns {@code true} when the value of the
+ * opcode is in between 0x8 and 0xF.
+ *
+ *
+ * @return {@code true} if this frame is a control frame
+ * (= if the opcode is in between 0x8 and 0xF).
+ */
+ public boolean isControlFrame() {
+ return (0x8 <= mOpcode && mOpcode <= 0xF);
+ }
+
+
+ /**
+ * Get the value of MASK bit.
+ *
+ * @return The value of MASK bit.
+ */
+ boolean getMask() {
+ return mMask;
+ }
+
+
+ /**
+ * Set the value of MASK bit.
+ *
+ * @param mask The value of MASK bit.
+ * @return {@code this} object.
+ */
+ WebSocketFrame setMask(boolean mask) {
+ mMask = mask;
+
+ return this;
+ }
+
+
+ /**
+ * Check if this frame has payload.
+ *
+ * @return {@code true} if this frame has payload.
+ */
+ public boolean hasPayload() {
+ return mPayload != null;
+ }
+
+
+ /**
+ * Get the payload length.
+ *
+ * @return The payload length.
+ */
+ public int getPayloadLength() {
+ if (mPayload == null) {
+ return 0;
+ }
+
+ return mPayload.length;
+ }
+
+
+ /**
+ * Get the unmasked payload.
+ *
+ * @return The unmasked payload. {@code null} may be returned.
+ */
+ public byte[] getPayload() {
+ return mPayload;
+ }
+
+
+ /**
+ * Get the unmasked payload as a text.
+ *
+ * @return A string constructed by interrupting the payload
+ * as a UTF-8 bytes.
+ */
+ public String getPayloadText() {
+ if (mPayload == null) {
+ return null;
+ }
+
+ return Misc.toStringUTF8(mPayload);
+ }
+
+
+ /**
+ * Set the unmasked payload.
+ *
+ *
+ * Note that the payload length of a control frame must be 125 bytes or less.
+ *
+ *
+ * @param payload The unmasked payload. {@code null} is accepted.
+ * An empty byte array is treated in the same way
+ * as {@code null}.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setPayload(byte[] payload) {
+ if (payload != null && payload.length == 0) {
+ payload = null;
+ }
+
+ mPayload = payload;
+
+ return this;
+ }
+
+
+ /**
+ * Set the payload. The given string is converted to a byte array
+ * in UTF-8 encoding.
+ *
+ *
+ * Note that the payload length of a control frame must be 125 bytes or less.
+ *
+ *
+ * @param payload The unmasked payload. {@code null} is accepted.
+ * An empty string is treated in the same way as
+ * {@code null}.
+ * @return {@code this} object.
+ */
+ public WebSocketFrame setPayload(String payload) {
+ if (payload == null || payload.length() == 0) {
+ return setPayload((byte[]) null);
+ }
+
+ return setPayload(Misc.getBytesUTF8(payload));
+ }
+
+
+ /**
+ * Set the payload that conforms to the payload format of close frames.
+ *
+ *
+ * The given parameters are encoded based on the rules described in
+ * "5.5.1. Close" of RFC 6455.
+ *
+ *
+ *
+ * Note that the reason should not be too long because the payload
+ * length of a control frame must be 125 bytes or less.
+ *
+ *
+ * @param closeCode The close code.
+ * @param reason The reason. {@code null} is accepted. An empty string
+ * is treated in the same way as {@code null}.
+ * @return {@code this} object.
+ * @see RFC 6455, 5.5.1. Close
+ * @see WebSocketCloseCode
+ */
+ public WebSocketFrame setCloseFramePayload(int closeCode, String reason) {
+ // Convert the close code to a 2-byte unsigned integer
+ // in network byte order.
+ byte[] encodedCloseCode = new byte[]{
+ (byte) ((closeCode >> 8) & 0xFF), (byte) ((closeCode) & 0xFF)
+ };
+
+ // If a reason string is not given.
+ if (reason == null || reason.length() == 0) {
+ // Use the close code only.
+ return setPayload(encodedCloseCode);
+ }
+
+ // Convert the reason into a byte array.
+ byte[] encodedReason = Misc.getBytesUTF8(reason);
+
+ // Concatenate the close code and the reason.
+ byte[] payload = new byte[2 + encodedReason.length];
+ System.arraycopy(encodedCloseCode, 0, payload, 0, 2);
+ System.arraycopy(encodedReason, 0, payload, 2, encodedReason.length);
+
+ // Use the concatenated string.
+ return setPayload(payload);
+ }
+
+
+ /**
+ * Parse the first two bytes of the payload as a close code.
+ *
+ *
+ * If any payload is not set or the length of the payload is less than 2,
+ * this method returns 1005 ({@link WebSocketCloseCode#NONE}).
+ *
+ *
+ *
+ * The value returned from this method is meaningless if this frame
+ * is not a close frame.
+ *
+ *
+ * @return The close code.
+ * @see RFC 6455, 5.5.1. Close
+ * @see WebSocketCloseCode
+ */
+ public int getCloseCode() {
+ if (mPayload == null || mPayload.length < 2) {
+ return WebSocketCloseCode.NONE;
+ }
+
+ // A close code is encoded in network byte order.
+ int closeCode = (((mPayload[0] & 0xFF) << 8) | (mPayload[1] & 0xFF));
+
+ return closeCode;
+ }
+
+
+ /**
+ * Parse the third and subsequent bytes of the payload as a close reason.
+ *
+ *
+ * If any payload is not set or the length of the payload is less than 3,
+ * this method returns {@code null}.
+ *
+ *
+ *
+ * The value returned from this method is meaningless if this frame
+ * is not a close frame.
+ *
+ *
+ * @return The close reason.
+ */
+ public String getCloseReason() {
+ if (mPayload == null || mPayload.length < 3) {
+ return null;
+ }
+
+ return Misc.toStringUTF8(mPayload, 2, mPayload.length - 2);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder().append("WebSocketFrame(FIN=").append(mFin ? "1" : "0").append(",RSV1=").append(mRsv1 ? "1" : "0").append(",RSV2=").append(mRsv2 ? "1" : "0").append(",RSV3=").append(mRsv3 ? "1" : "0").append(",Opcode=").append(Misc.toOpcodeName(mOpcode)).append(",Length=").append(getPayloadLength());
+
+ switch (mOpcode) {
+ case TEXT:
+ appendPayloadText(builder);
+ break;
+
+ case BINARY:
+ appendPayloadBinary(builder);
+ break;
+
+ case CLOSE:
+ appendPayloadClose(builder);
+ break;
+ }
+
+ return builder.append(")").toString();
+ }
+
+
+ private boolean appendPayloadCommon(StringBuilder builder) {
+ builder.append(",Payload=");
+
+ if (mPayload == null) {
+ builder.append("null");
+
+ // Nothing more to append.
+ return true;
+ }
+
+ if (mRsv1) {
+ // In the current implementation, mRsv1=true is allowed
+ // only when Per-Message Compression is applied.
+ builder.append("compressed");
+
+ // Nothing more to append.
+ return true;
+ }
+
+ // Continue.
+ return false;
+ }
+
+
+ private void appendPayloadText(StringBuilder builder) {
+ if (appendPayloadCommon(builder)) {
+ // Nothing more to append.
+ return;
+ }
+
+ builder.append("\"");
+ builder.append(getPayloadText());
+ builder.append("\"");
+ }
+
+
+ private void appendPayloadClose(StringBuilder builder) {
+ builder.append(",CloseCode=").append(getCloseCode()).append(",Reason=");
+
+ String reason = getCloseReason();
+
+ if (reason == null) {
+ builder.append("null");
+ } else {
+ builder.append("\"").append(reason).append("\"");
+ }
+ }
+
+
+ private void appendPayloadBinary(StringBuilder builder) {
+ if (appendPayloadCommon(builder)) {
+ // Nothing more to append.
+ return;
+ }
+
+ for (int i = 0; i < mPayload.length; ++i) {
+ builder.append(String.format("%02X ", (0xFF & mPayload[i])));
+ }
+
+ if (mPayload.length != 0) {
+ // Remove the last space.
+ builder.setLength(builder.length() - 1);
+ }
+ }
+
+
+ /**
+ * Create a continuation frame. Note that the FIN bit of the
+ * returned frame is false.
+ *
+ * @return A WebSocket frame whose FIN bit is false, opcode is
+ * {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
+ * payload is {@code null}.
+ */
+ public static WebSocketFrame createContinuationFrame() {
+ return new WebSocketFrame().setOpcode(CONTINUATION);
+ }
+
+
+ /**
+ * Create a continuation frame. Note that the FIN bit of the
+ * returned frame is false.
+ *
+ * @param payload The payload for a newly create frame.
+ * @return A WebSocket frame whose FIN bit is false, opcode is
+ * {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
+ * payload is the given one.
+ */
+ public static WebSocketFrame createContinuationFrame(byte[] payload) {
+ return createContinuationFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Create a continuation frame. Note that the FIN bit of the
+ * returned frame is false.
+ *
+ * @param payload The payload for a newly create frame.
+ * @return A WebSocket frame whose FIN bit is false, opcode is
+ * {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
+ * payload is the given one.
+ */
+ public static WebSocketFrame createContinuationFrame(String payload) {
+ return createContinuationFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Create a text frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#TEXT TEXT} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createTextFrame(String payload) {
+ return new WebSocketFrame().setFin(true).setOpcode(TEXT).setPayload(payload);
+ }
+
+
+ /**
+ * Create a binary frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#BINARY BINARY} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createBinaryFrame(byte[] payload) {
+ return new WebSocketFrame().setFin(true).setOpcode(BINARY).setPayload(payload);
+ }
+
+ public static WebSocketFrame createBinaryFrame(byte[] payload, Object requestWrapper) {
+ return new WebSocketFrame().setFin(true).setOpcode(BINARY).setPayload(payload).setRequestWrapper(requestWrapper);
+ }
+
+ /**
+ * Create a close frame.
+ *
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#CLOSE CLOSE} and payload is
+ * {@code null}.
+ */
+ public static WebSocketFrame createCloseFrame() {
+ return new WebSocketFrame().setFin(true).setOpcode(CLOSE);
+ }
+
+
+ /**
+ * Create a close frame.
+ *
+ * @param closeCode The close code.
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#CLOSE CLOSE} and payload
+ * contains a close code.
+ * @see WebSocketCloseCode
+ */
+ public static WebSocketFrame createCloseFrame(int closeCode) {
+ return createCloseFrame().setCloseFramePayload(closeCode, null);
+ }
+
+
+ /**
+ * Create a close frame.
+ *
+ * @param closeCode The close code.
+ * @param reason The close reason.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#CLOSE CLOSE} and payload
+ * contains a close code and a close reason.
+ * @see WebSocketCloseCode
+ */
+ public static WebSocketFrame createCloseFrame(int closeCode, String reason) {
+ return createCloseFrame().setCloseFramePayload(closeCode, reason);
+ }
+
+
+ /**
+ * Create a ping frame.
+ *
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PING PING} and payload is
+ * {@code null}.
+ */
+ public static WebSocketFrame createPingFrame() {
+ return new WebSocketFrame().setFin(true).setOpcode(PING);
+ }
+
+
+ /**
+ * Create a ping frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PING PING} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createPingFrame(byte[] payload) {
+ return createPingFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Create a ping frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PING PING} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createPingFrame(String payload) {
+ return createPingFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Create a pong frame.
+ *
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PONG PONG} and payload is
+ * {@code null}.
+ */
+ public static WebSocketFrame createPongFrame() {
+ return new WebSocketFrame().setFin(true).setOpcode(PONG);
+ }
+
+
+ /**
+ * Create a pong frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PONG PONG} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createPongFrame(byte[] payload) {
+ return createPongFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Create a pong frame.
+ *
+ * @param payload The payload for a newly created frame.
+ * Note that a control frame's payload length must be 125 bytes or less
+ * (RFC 6455, 5.5. Control Frames).
+ * @return A WebSocket frame whose FIN bit is true, opcode is
+ * {@link WebSocketOpcode#PONG PONG} and payload is
+ * the given one.
+ */
+ public static WebSocketFrame createPongFrame(String payload) {
+ return createPongFrame().setPayload(payload);
+ }
+
+
+ /**
+ * Mask/unmask payload.
+ *
+ *
+ *
+ * @param maskingKey The masking key. If {@code null} is given or the length
+ * of the masking key is less than 4, nothing is performed.
+ * @param payload Payload to be masked/unmasked.
+ * @return {@code payload}.
+ * @see 5.3. Client-to-Server Masking
+ */
+ static byte[] mask(byte[] maskingKey, byte[] payload) {
+ if (maskingKey == null || maskingKey.length < 4 || payload == null) {
+ return payload;
+ }
+
+ for (int i = 0; i < payload.length; ++i) {
+ payload[i] ^= maskingKey[i % 4];
+ }
+
+ return payload;
+ }
+
+
+ static WebSocketFrame compressFrame(WebSocketFrame frame, PerMessageCompressionExtension pmce) {
+ // If Per-Message Compression is not enabled.
+ if (pmce == null) {
+ // No compression.
+ return frame;
+ }
+
+ // If the frame is neither a TEXT frame nor a BINARY frame.
+ if (frame.isTextFrame() == false && frame.isBinaryFrame() == false) {
+ // No compression.
+ return frame;
+ }
+
+ // If the frame is not the final frame.
+ if (frame.getFin() == false) {
+ // The compression must be applied to this frame and
+ // all the subsequent continuation frames, but the
+ // current implementation does not support the behavior.
+ return frame;
+ }
+
+ // If the RSV1 bit is set.
+ if (frame.getRsv1()) {
+ // In the current implementation, RSV1=true is allowed
+ // only as Per-Message Compressed Bit (See RFC 7692,
+ // 6. Framing). Therefore, RSV1=true here is regarded
+ // as "already compressed".
+ return frame;
+ }
+
+ // The plain payload before compression.
+ byte[] payload = frame.getPayload();
+
+ // If the payload is empty.
+ if (payload == null || payload.length == 0) {
+ // No compression.
+ return frame;
+ }
+
+ // Compress the payload.
+ byte[] compressed = compress(payload, pmce);
+
+ // If the length of the compressed data is not less than
+ // that of the original plain payload.
+ if (payload.length <= compressed.length) {
+ // It's better not to compress the payload.
+ return frame;
+ }
+
+ // Replace the plain payload with the compressed data.
+ frame.setPayload(compressed);
+
+ // Set Per-Message Compressed Bit (See RFC 7692, 6. Framing).
+ frame.setRsv1(true);
+
+ return frame;
+ }
+
+
+ private static byte[] compress(byte[] data, PerMessageCompressionExtension pmce) {
+ try {
+ // Compress the data.
+ return pmce.compress(data);
+ } catch (WebSocketException e) {
+ // Failed to compress the data. Ignore this error and use
+ // the plain original data. The current implementation
+ // does not call any listener callback method for this error.
+ return data;
+ }
+ }
+
+
+ static List splitIfNecessary(WebSocketFrame frame, int maxPayloadSize, PerMessageCompressionExtension pmce) {
+ // If the maximum payload size is not specified.
+ if (maxPayloadSize == 0) {
+ // Not split.
+ return null;
+ }
+
+ // If the total length of the payload is equal to or
+ // less than the maximum payload size.
+ if (frame.getPayloadLength() <= maxPayloadSize) {
+ // Not split.
+ return null;
+ }
+
+ // If the frame is a binary frame or a text frame.
+ if (frame.isBinaryFrame() || frame.isTextFrame()) {
+ // Try to compress the frame. In the current implementation, binary
+ // frames and text frames with the FIN bit true can be compressed.
+ // The compressFrame() method may change the payload and the RSV1
+ // bit of the given frame.
+ frame = compressFrame(frame, pmce);
+
+ // If the payload length of the frame has become equal to or less
+ // than the maximum payload size as a result of the compression.
+ if (frame.getPayloadLength() <= maxPayloadSize) {
+ // Not split. (Note that the frame has been compressed)
+ return null;
+ }
+ } else if (frame.isContinuationFrame() == false) {
+ // Control frames (Close/Ping/Pong) are not split.
+ return null;
+ }
+
+ // Split the frame.
+ return split(frame, maxPayloadSize);
+ }
+
+
+ private static List split(WebSocketFrame frame, int maxPayloadSize) {
+ // The original payload and the original FIN bit.
+ byte[] originalPayload = frame.getPayload();
+ boolean originalFin = frame.getFin();
+
+ List frames = new ArrayList();
+
+ // Generate the first frame using the existing WebSocketFrame instance.
+ // Note that the reserved bit 1 and the opcode are untouched.
+ byte[] payload = Arrays.copyOf(originalPayload, maxPayloadSize);
+ frame.setFin(false).setPayload(payload);
+ frames.add(frame);
+
+ for (int from = maxPayloadSize; from < originalPayload.length; from += maxPayloadSize) {
+ // Prepare the payload of the next continuation frame.
+ int to = Math.min(from + maxPayloadSize, originalPayload.length);
+ payload = Arrays.copyOfRange(originalPayload, from, to);
+
+ // Create a continuation frame.
+ WebSocketFrame cont = WebSocketFrame.createContinuationFrame(payload);
+ frames.add(cont);
+ }
+
+ if (originalFin) {
+ // Set the FIN bit of the last frame.
+ frames.get(frames.size() - 1).setFin(true);
+ }
+
+ return frames;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketInputStream.java b/src/main/java/com/neovisionaries/ws/client/WebSocketInputStream.java
new file mode 100644
index 0000000..14b0b3c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketInputStream.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+
+class WebSocketInputStream extends FilterInputStream {
+ public WebSocketInputStream(InputStream in) {
+ super(in);
+ }
+
+
+ public String readLine() throws IOException {
+ return Misc.readLine(this, "UTF-8");
+ }
+
+
+ public WebSocketFrame readFrame() throws IOException, WebSocketException {
+ // Buffer.
+ byte[] buffer = new byte[8];
+
+ try {
+ // Read the first two bytes.
+ readBytes(buffer, 2);
+ } catch (InsufficientDataException e) {
+ if (e.getReadByteCount() == 0) {
+ // The connection has been closed without receiving a close frame.
+ // Strictly speaking, this is a violation against RFC 6455.
+ throw new NoMoreFrameException();
+ } else {
+ // Re-throw the exception.
+ throw e;
+ }
+ }
+
+ // FIN
+ boolean fin = ((buffer[0] & 0x80) != 0);
+
+ // RSV1, RSV2, RSV3
+ boolean rsv1 = ((buffer[0] & 0x40) != 0);
+ boolean rsv2 = ((buffer[0] & 0x20) != 0);
+ boolean rsv3 = ((buffer[0] & 0x10) != 0);
+
+ // Opcode
+ int opcode = (buffer[0] & 0x0F);
+
+ // Mask flag. This should never be true because the specification
+ // (RFC 6455, 5. Data Framing, 5.1. Overview) says as follows:
+ //
+ // A server MUST NOT mask any frames that it sends to the client.
+ //
+ boolean mask = ((buffer[1] & 0x80) != 0);
+
+ // The payload length. It is expressed in 7 bits.
+ long payloadLength = buffer[1] & 0x7F;
+
+ if (payloadLength == 126) {
+ // Read the extended payload length.
+ // It is expressed in 2 bytes in network byte order.
+ readBytes(buffer, 2);
+
+ // Interpret the bytes as a number.
+ payloadLength = (((buffer[0] & 0xFF) << 8) | ((buffer[1] & 0xFF)));
+ } else if (payloadLength == 127) {
+ // Read the extended payload length.
+ // It is expressed in 8 bytes in network byte order.
+ readBytes(buffer, 8);
+
+ // From RFC 6455, p29.
+ //
+ // the most significant bit MUST be 0
+ //
+ if ((buffer[0] & 0x80) != 0) {
+ // The payload length in a frame is invalid.
+ throw new WebSocketException(WebSocketError.INVALID_PAYLOAD_LENGTH, "The payload length of a frame is invalid.");
+ }
+
+ // Interpret the bytes as a number.
+ payloadLength = (((buffer[0] & 0xFF) << 56) |
+ ((buffer[1] & 0xFF) << 48) |
+ ((buffer[2] & 0xFF) << 40) |
+ ((buffer[3] & 0xFF) << 32) |
+ ((buffer[4] & 0xFF) << 24) |
+ ((buffer[5] & 0xFF) << 16) |
+ ((buffer[6] & 0xFF) << 8) |
+ ((buffer[7] & 0xFF)));
+ }
+
+ // Masking key
+ byte[] maskingKey = null;
+
+ if (mask) {
+ // Read the masking key. (This should never happen.)
+ maskingKey = new byte[4];
+ readBytes(maskingKey, 4);
+ }
+
+ if (Integer.MAX_VALUE < payloadLength) {
+ // In Java, the maximum array size is Integer.MAX_VALUE.
+ // Skip the payload and raise an exception.
+ skipQuietly(payloadLength);
+ throw new WebSocketException(WebSocketError.TOO_LONG_PAYLOAD, "The payload length of a frame exceeds the maximum array size in Java.");
+ }
+
+ // Read the payload if the payload length is not 0.
+ byte[] payload = readPayload(payloadLength, mask, maskingKey);
+
+ // Create a WebSocketFrame instance that represents a frame.
+ return new WebSocketFrame().setFin(fin).setRsv1(rsv1).setRsv2(rsv2).setRsv3(rsv3).setOpcode(opcode).setMask(mask).setPayload(payload);
+ }
+
+
+ void readBytes(byte[] buffer, int length) throws IOException, WebSocketException {
+ // Read
+ int total = 0;
+
+ while (total < length) {
+ int count = read(buffer, total, length - total);
+
+ if (count <= 0) {
+ // The end of the stream has been reached unexpectedly.
+ throw new InsufficientDataException(length, total);
+ }
+
+ total += count;
+ }
+ }
+
+
+ private void skipQuietly(long length) {
+ try {
+ skip(length);
+ } catch (IOException e) {
+ }
+ }
+
+
+ private byte[] readPayload(long payloadLength, boolean mask, byte[] maskingKey) throws IOException, WebSocketException {
+ if (payloadLength == 0) {
+ return null;
+ }
+
+ byte[] payload;
+
+ try {
+ // Allocate a memory area to hold the content of the payload.
+ payload = new byte[(int) payloadLength];
+ } catch (OutOfMemoryError e) {
+ // OutOfMemoryError occurred during a trial to allocate a memory area
+ // for a frame's payload. Skip the payload and raise an exception.
+ skipQuietly(payloadLength);
+ throw new WebSocketException(WebSocketError.INSUFFICIENT_MEMORY_FOR_PAYLOAD, "OutOfMemoryError occurred during a trial to allocate a memory area for a frame's payload: " + e.getMessage(), e);
+ }
+
+ // Read the payload.
+ readBytes(payload, payload.length);
+
+ // If masked.
+ if (mask) {
+ // Unmasked the payload.
+ WebSocketFrame.mask(maskingKey, payload);
+ }
+
+ return payload;
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketListener.java b/src/main/java/com/neovisionaries/ws/client/WebSocketListener.java
new file mode 100644
index 0000000..c63bba6
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketListener.java
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Listener interface to receive WebSocket events.
+ *
+ *
+ * An implementation of this interface should be added by {@link
+ * WebSocket#addListener(WebSocketListener)} to a {@link WebSocket}
+ * instance before calling {@link WebSocket#connect()}.
+ *
+ *
+ *
+ * {@link WebSocketAdapter} is an empty implementation of this interface.
+ *
+ *
+ * @see WebSocket#addListener(WebSocketListener)
+ * @see WebSocketAdapter
+ */
+public interface WebSocketListener {
+ /**
+ * Called after the state of the WebSocket changed.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param newState
+ * The new state of the WebSocket.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ *
+ * @since 1.1
+ */
+ void onStateChanged(WebSocket websocket, WebSocketState newState) throws Exception;
+
+
+ /**
+ * Called after the opening handshake of the WebSocket connection succeeded.
+ *
+ * @param websocket
+ * The WebSsocket.
+ *
+ * @param headers
+ * HTTP headers received from the server. Keys of the map are
+ * HTTP header names such as {@code "Sec-WebSocket-Accept"}.
+ * Note that the comparator used by the map is {@link
+ * String#CASE_INSENSITIVE_ORDER}.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onConnected(WebSocket websocket, Map> headers) throws Exception;
+
+
+ /**
+ * Called when {@link WebSocket#connectAsynchronously()} failed.
+ *
+ *
+ * Note that this method is called only when {@code connectAsynchronously()}
+ * was used and the {@link WebSocket#connect() connect()} executed in the
+ * background thread failed. Neither direct synchronous {@code connect()}
+ * nor {@link WebSocket#connect(java.util.concurrent.ExecutorService)
+ * connect(ExecutorService)} will trigger this callback method.
+ *
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * The exception thrown by {@link WebSocket#connect() connect()}
+ * method.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ *
+ * @since 1.8
+ */
+ void onConnectError(WebSocket websocket, WebSocketException cause) throws Exception;
+
+
+ /**
+ * Called after the WebSocket connection was closed.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param serverCloseFrame
+ * The close frame which the server sent to this client.
+ * This may be {@code null}.
+ *
+ * @param clientCloseFrame
+ * The close frame which this client sent to the server.
+ * This may be {@code null}.
+ *
+ * @param closedByServer
+ * {@code true} if the closing handshake was started by the server.
+ * {@code false} if the closing handshake was started by the client.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception;
+
+
+ /**
+ * Called when a frame was received. This method is called before
+ * an onXxxFrame method is called.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a continuation frame (opcode = 0x0) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The continuation frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onContinuationFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a text frame (opcode = 0x1) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The text frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a binary frame (opcode = 0x2) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The binary frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onBinaryFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a close frame (opcode = 0x8) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The close frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onCloseFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a ping frame (opcode = 0x9) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The ping frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onPingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a pong frame (opcode = 0xA) was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The pong frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onPongFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a text message was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param text
+ * The text message.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onTextMessage(WebSocket websocket, String text) throws Exception;
+
+
+ /**
+ * Called when a binary message was received.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param binary
+ * The binary message.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception;
+
+
+ /**
+ * Called before a WebSocket frame is sent.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The WebSocket frame to be sent.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ *
+ * @since 1.15
+ */
+ void onSendingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a WebSocket frame was sent to the server
+ * (but not flushed yet).
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The sent frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when a WebSocket frame was not sent to the server
+ * because a close frame has already been sent.
+ *
+ *
+ * Note that {@code onFrameUnsent} is not called when {@link
+ * #onSendError(WebSocket, WebSocketException, WebSocketFrame)
+ * onSendError} is called.
+ *
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param frame
+ * The unsent frame.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onFrameUnsent(WebSocket websocket, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called between after a thread is created and before the thread's
+ * {@code start()} method is called.
+ *
+ * @param websocket The WebSocket.
+ * @param threadType The thread type.
+ * @param thread The newly created thread instance.
+ * @throws Exception An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ * @since 2.0
+ */
+ void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception;
+
+
+ /**
+ * Called at the very beginning of the thread's {@code run()} method implementation.
+ *
+ * @param websocket The WebSocket.
+ * @param threadType The thread type.
+ * @param thread The thread instance.
+ * @throws Exception An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ * @since 2.0
+ */
+ void onThreadStarted(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception;
+
+
+ /**
+ * Called at the very end of the thread's {@code run()} method implementation.
+ *
+ * @param websocket The WebSocket.
+ * @param threadType The thread type.
+ * @param thread The thread instance.
+ * @throws Exception An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ * @since 2.0
+ */
+ void onThreadStopping(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception;
+
+
+ /**
+ * Call when an error occurred. This method is called before
+ * an onXxxError method is called.
+ *
+ * @param websocket The WebSocket.
+ * @param cause An exception that represents the error.
+ * @throws Exception An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onError(WebSocket websocket, WebSocketException cause) throws Exception;
+
+
+ /**
+ * Called when a WebSocket frame failed to be read from the WebSocket.
+ *
+ *
+ * Some WebSocket server implementations close a WebSocket connection without sending
+ * a close frame to a
+ * client in some cases. Strictly speaking, this behavior is a violation against the
+ * specification (RFC 6455). However,
+ * this library has allowed the behavior by default since the version 1.29. Even if
+ * the end of the input stream of a WebSocket connection were reached without a
+ * close frame being received, it would trigger neither {@link #onError(WebSocket,
+ * WebSocketException) onError()} method nor {@link #onFrameError(WebSocket,
+ * WebSocketException, WebSocketFrame) onFrameError()} method. If you want to make
+ * this library report an error in the case, pass {@code false} to {@link
+ * WebSocket#setMissingCloseFrameAllowed(boolean)} method.
+ *
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * An exception that represents the error. When the error occurred
+ * because of {@link java.io.InterruptedIOException InterruptedIOException},
+ * {@code exception.getError()} returns {@link WebSocketError#INTERRUPTED_IN_READING}.
+ * For other IO errors, {@code exception.getError()} returns {@link
+ * WebSocketError#IO_ERROR_IN_READING}. Other error codes denote
+ * protocol errors, which imply that some bugs may exist in either
+ * or both of the client-side and the server-side implementations.
+ *
+ * @param frame
+ * The WebSocket frame. If this is not {@code null}, it means that
+ * verification of the frame failed.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onFrameError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when it failed to concatenate payloads of multiple frames
+ * to construct a message. The reason of the failure is probably
+ * out-of-memory.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * An exception that represents the error.
+ *
+ * @param frames
+ * The list of frames that form a message. The first element
+ * is either a text frame and a binary frame, and the other
+ * frames are continuation frames.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onMessageError(WebSocket websocket, WebSocketException cause, List frames) throws Exception;
+
+
+ /**
+ * Called when a message failed to be decompressed.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * An exception that represents the error.
+ *
+ * @param compressed
+ * The compressed message that failed to be decompressed.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ *
+ * @since 1.16
+ */
+ void onMessageDecompressionError(WebSocket websocket, WebSocketException cause, byte[] compressed) throws Exception;
+
+
+ /**
+ * Called when it failed to convert payload data into a string.
+ * The reason of the failure is probably out-of-memory.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * An exception that represents the error.
+ *
+ * @param data
+ * The payload data that failed to be converted to a string.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onTextMessageError(WebSocket websocket, WebSocketException cause, byte[] data) throws Exception;
+
+
+ /**
+ * Called when an error occurred when a frame was tried to be sent
+ * to the server.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * An exception that represents the error.
+ *
+ * @param frame
+ * The frame which was tried to be sent. This is {@code null}
+ * when the error code of the exception is {@link
+ * WebSocketError#FLUSH_ERROR FLUSH_ERROR}.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onSendError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws Exception;
+
+
+ /**
+ * Called when an uncaught throwable was detected in either the
+ * reading thread (which reads frames from the server) or the
+ * writing thread (which sends frames to the server).
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * The cause of the error.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ */
+ void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws Exception;
+
+
+ /**
+ * Called when an onXxx() method threw a {@code Throwable}.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param cause
+ * The {@code Throwable} an onXxx method threw.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is just ignored.
+ *
+ * @since 1.9
+ */
+ void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception;
+
+
+ /**
+ * Called before an opening handshake is sent to the server.
+ *
+ * @param websocket
+ * The WebSocket.
+ *
+ * @param requestLine
+ * The request line. For example, {@code "GET /chat HTTP/1.1"}.
+ *
+ * @param headers
+ * The HTTP headers.
+ *
+ * @throws Exception
+ * An exception thrown by an implementation of this method.
+ * The exception is passed to {@link #handleCallbackError(WebSocket, Throwable)}.
+ *
+ * @since 1.21
+ */
+ void onSendingHandshake(WebSocket websocket, String requestLine, List headers) throws Exception;
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketOpcode.java b/src/main/java/com/neovisionaries/ws/client/WebSocketOpcode.java
new file mode 100644
index 0000000..05bdb52
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketOpcode.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * Opcode.
+ *
+ * @see RFC 6455, 5.2. Base Framing Protocol
+ */
+public class WebSocketOpcode {
+ /**
+ * Opcode for "frame continuation" (0x0).
+ */
+ public static final int CONTINUATION = 0x0;
+
+
+ /**
+ * Opcode for "text frame" (0x1).
+ */
+ public static final int TEXT = 0x1;
+
+
+ /**
+ * Opcode for "binary frame" (0x2).
+ */
+ public static final int BINARY = 0x2;
+
+
+ /**
+ * Opcode for "connection close" (0x8).
+ */
+ public static final int CLOSE = 0x8;
+
+
+ /**
+ * Opcode for "ping" (0x9).
+ */
+ public static final int PING = 0x9;
+
+
+ /**
+ * Opcode for "pong" (0xA).
+ */
+ public static final int PONG = 0xA;
+
+
+ private WebSocketOpcode() {
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketOutputStream.java b/src/main/java/com/neovisionaries/ws/client/WebSocketOutputStream.java
new file mode 100644
index 0000000..3a63e52
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketOutputStream.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+class WebSocketOutputStream extends FilterOutputStream {
+ public WebSocketOutputStream(OutputStream out) {
+ super(out);
+ }
+
+
+ public void write(String string) throws IOException {
+ // Convert the string into a byte array.
+ byte[] bytes = Misc.getBytesUTF8(string);
+
+ write(bytes);
+ }
+
+
+ public void write(WebSocketFrame frame) throws IOException {
+ writeFrame0(frame);
+ writeFrame1(frame);
+ writeFrameExtendedPayloadLength(frame);
+
+ // Generate a random masking key.
+
+ // Write the masking key.
+ if (WebSocket.useMask) {
+ byte[] maskingKey = Misc.nextBytes(4);
+ write(maskingKey);
+
+ // Write the payload.
+ writeFramePayload(frame, maskingKey);
+ } else {
+ writeFramePayload(frame, null);
+ }
+ }
+
+
+ private void writeFrame0(WebSocketFrame frame) throws IOException {
+ int b = (frame.getFin() ? 0x80 : 0x00) | (frame.getRsv1() ? 0x40 : 0x00) | (frame.getRsv2() ? 0x20 : 0x00) | (frame.getRsv3() ? 0x10 : 0x00) | (frame.getOpcode() & 0x0F);
+
+ write(b);
+ }
+
+
+ private void writeFrame1(WebSocketFrame frame) throws IOException {
+ // Frames sent from a client are always masked.
+ int b;
+ if (WebSocket.useMask) {
+ b = 0x80;
+ } else {
+ b = 0x00;
+ }
+
+ int len = frame.getPayloadLength();
+
+ if (len <= 125) {
+ b |= len;
+ } else if (len <= 65535) {
+ b |= 126;
+ } else {
+ b |= 127;
+ }
+
+ write(b);
+ }
+
+
+ private void writeFrameExtendedPayloadLength(WebSocketFrame frame) throws IOException {
+ int len = frame.getPayloadLength();
+
+ if (len <= 125) {
+ return;
+ }
+
+ if (len <= 65535) {
+ // 2-byte in network byte order.
+ write((len >> 8) & 0xFF);
+ write((len) & 0xFF);
+ return;
+ }
+
+ // In this implementation, the maximum payload length is (2^31 - 1).
+ // So, the first 4 bytes are 0.
+ write(0);
+ write(0);
+ write(0);
+ write(0);
+ write((len >> 24) & 0xFF);
+ write((len >> 16) & 0xFF);
+ write((len >> 8) & 0xFF);
+ write((len) & 0xFF);
+ }
+
+
+ private void writeFramePayload(WebSocketFrame frame, byte[] maskingKey) throws IOException {
+ byte[] payload = frame.getPayload();
+
+ if (payload == null) {
+ return;
+ }
+
+ for (int i = 0; i < payload.length; ++i) {
+ // Mask
+ int b;
+ if (WebSocket.useMask) {
+ b = (payload[i] ^ maskingKey[i % 4]) & 0xFF;
+ } else {
+ b = (payload[i]) & 0xFF;
+ }
+
+ // Write
+ write(b);
+ }
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketState.java b/src/main/java/com/neovisionaries/ws/client/WebSocketState.java
new file mode 100644
index 0000000..e9e384d
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketState.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015-2016 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+/**
+ * WebSocket state.
+ *
+ *
+ * The initial state of a {@link WebSocket} instance is
+ * CREATED. {@code WebSocket.}{@link
+ * WebSocket#connect() connect()} method is allowed to be called
+ * only when the state is {@code CREATED}. If the method is called
+ * when the state is not {@code CREATED}, a {@link WebSocketException}
+ * is thrown (its error code is {@link WebSocketError#NOT_IN_CREATED_STATE
+ * NOT_IN_CREATED_STATE}).
+ *
+ *
+ *
+ * At the beginning of the implementation of {@code connect()} method,
+ * the state is changed to CONNECTING, and then
+ * {@link WebSocketListener#onStateChanged(WebSocket, WebSocketState)
+ * onStateChanged()} method of each registered listener ({@link
+ * WebSocketListener}) is called.
+ *
+ *
+ *
+ * After the state is changed to {@code CONNECTING}, a WebSocket
+ * opening
+ * handshake is performed. If an error occurred during the
+ * handshake, the state is changed to {@code CLOSED} ({@code
+ * onStateChanged()} method of listeners is called) and a {@code
+ * WebSocketException} is thrown. There are various reasons for
+ * handshake failure. If you want to know the reason, get the error
+ * code ({@link WebSocketError}) by calling {@link
+ * WebSocketException#getError() getError()} method of the exception.
+ *
+ *
+ *
+ * After the opening handshake succeeded, the state is changed to
+ * OPEN. Listeners' {@code onStateChanged()} method
+ * and {@link WebSocketListener#onConnected(WebSocket, java.util.Map)
+ * onConnected()} method are called in this order. Note that {@code
+ * onConnected()} method is called by another thread.
+ *
+ *
+ *
+ * Upon either sending or receiving a close frame,
+ * a closing
+ * handshake is started. The state is changed to
+ * CLOSING and {@code onStateChanged()} method of
+ * listeners is called.
+ *
+ *
+ *
+ * After the client and the server have exchanged close frames, the
+ * state is changed to CLOSED. Listeners'
+ * {@code onStateChanged()} method and {@link
+ * WebSocketListener#onDisconnected(WebSocket, WebSocketFrame,
+ * WebSocketFrame, boolean) onDisconnected()} method is called in
+ * this order.
+ *
+ */
+public enum WebSocketState {
+ /**
+ * The initial state of a {@link WebSocket} instance.
+ */
+ CREATED,
+
+
+ /**
+ * An opening
+ * handshake is being performed.
+ */
+ CONNECTING,
+
+
+ /**
+ * The WebSocket connection is established (= the opening handshake
+ * has succeeded) and usable.
+ */
+ OPEN,
+
+
+ /**
+ * A closing
+ * handshake is being performed.
+ */
+ CLOSING,
+
+
+ /**
+ * The WebSocket connection is closed.
+ */
+ CLOSED
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WebSocketThread.java b/src/main/java/com/neovisionaries/ws/client/WebSocketThread.java
new file mode 100644
index 0000000..8dff89d
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WebSocketThread.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+abstract class WebSocketThread extends Thread {
+ protected final WebSocket mWebSocket;
+ private final ThreadType mThreadType;
+
+
+ WebSocketThread(String name, WebSocket ws, ThreadType type) {
+ super(name);
+
+ mWebSocket = ws;
+ mThreadType = type;
+ }
+
+
+ @Override
+ public void run() {
+ ListenerManager lm = mWebSocket.getListenerManager();
+
+ if (lm != null) {
+ // Execute onThreadStarted() of the listeners.
+ lm.callOnThreadStarted(mThreadType, this);
+ }
+
+ runMain();
+
+ if (lm != null) {
+ // Execute onThreadStopping() of the listeners.
+ lm.callOnThreadStopping(mThreadType, this);
+ }
+ }
+
+
+ public void callOnThreadCreated() {
+ ListenerManager lm = mWebSocket.getListenerManager();
+
+ if (lm != null) {
+ lm.callOnThreadCreated(mThreadType, this);
+ }
+ }
+
+
+ protected abstract void runMain();
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/WritingThread.java b/src/main/java/com/neovisionaries/ws/client/WritingThread.java
new file mode 100644
index 0000000..e0fd896
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/WritingThread.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2015-2017 Neo Visionaries Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.neovisionaries.ws.client;
+
+
+import com.neovisionaries.ws.client.StateManager.CloseInitiator;
+import java.io.IOException;
+import java.util.LinkedList;
+
+import static com.neovisionaries.ws.client.WebSocketState.CLOSED;
+import static com.neovisionaries.ws.client.WebSocketState.CLOSING;
+
+
+class WritingThread extends WebSocketThread {
+ private static final int SHOULD_SEND = 0;
+ private static final int SHOULD_STOP = 1;
+ private static final int SHOULD_CONTINUE = 2;
+ private static final int SHOULD_FLUSH = 3;
+ private static final int FLUSH_THRESHOLD = 1000;
+ private final LinkedList mFrames;
+ private final PerMessageCompressionExtension mPMCE;
+ private boolean mStopRequested;
+ private WebSocketFrame mCloseFrame;
+ private boolean mFlushNeeded;
+ private boolean mStopped;
+
+
+ public WritingThread(WebSocket websocket) {
+ super("WritingThread", websocket, ThreadType.WRITING_THREAD);
+
+ mFrames = new LinkedList();
+ mPMCE = websocket.getPerMessageCompressionExtension();
+ }
+
+
+ @Override
+ public void runMain() {
+ try {
+ main();
+ } catch (Throwable t) {
+ // An uncaught throwable was detected in the writing thread.
+ WebSocketException cause = new WebSocketException(WebSocketError.UNEXPECTED_ERROR_IN_WRITING_THREAD, "An uncaught throwable was detected in the writing thread: " + t.getMessage(), t);
+
+ // Notify the listeners.
+ ListenerManager manager = mWebSocket.getListenerManager();
+ manager.callOnError(cause);
+ manager.callOnUnexpectedError(cause);
+ }
+
+ synchronized (this) {
+ // Mainly for queueFrame().
+ mStopped = true;
+ notifyAll();
+ }
+
+ // Notify this writing thread finished.
+ notifyFinished();
+ }
+
+
+ private void main() {
+ mWebSocket.onWritingThreadStarted();
+
+ while (true) {
+ // Wait for frames to be queued.
+ int result = waitForFrames();
+
+ if (result == SHOULD_STOP) {
+ break;
+ } else if (result == SHOULD_FLUSH) {
+ flushIgnoreError();
+ continue;
+ } else if (result == SHOULD_CONTINUE) {
+ continue;
+ }
+
+ try {
+ // Send frames.
+ sendFrames(false);
+ } catch (WebSocketException e) {
+ // An I/O error occurred.
+ break;
+ }
+ }
+
+ try {
+ // Send remaining frames, if any.
+ sendFrames(true);
+ } catch (WebSocketException e) {
+ // An I/O error occurred.
+ }
+ }
+
+
+ public void requestStop() {
+ synchronized (this) {
+ // Schedule stopping.
+ mStopRequested = true;
+
+ // Wake up this thread.
+ notifyAll();
+ }
+ }
+
+
+ public boolean queueFrame(WebSocketFrame frame) {
+ synchronized (this) {
+ while (true) {
+ // If this thread has already stopped.
+ if (mStopped) {
+ // Frames won't be sent any more. Not queued.
+ return false;
+ }
+
+ // If this thread has been requested to stop or has sent a
+ // close frame to the server.
+ if (mStopRequested || mCloseFrame != null) {
+ // Don't wait. Process the remaining task without delay.
+ break;
+ }
+
+ // If the frame is a control frame.
+ if (frame.isControlFrame()) {
+ // Queue the frame without blocking.
+ break;
+ }
+
+ // Get the upper limit of the queue size.
+ int queueSize = mWebSocket.getFrameQueueSize();
+
+ // If the upper limit is not set.
+ if (queueSize == 0) {
+ // Add the frame to mFrames unconditionally.
+ break;
+ }
+
+ // If the current queue size has not reached the upper limit.
+ if (mFrames.size() < queueSize) {
+ // Add the frame.
+ break;
+ }
+
+ try {
+ // Wait until the queue gets spaces.
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ // Add the frame to the queue.
+ if (isHighPriorityFrame(frame)) {
+ // Add the frame at the first position so that it can be sent immediately.
+ addHighPriorityFrame(frame);
+ } else {
+ // Add the frame at the last position.
+ mFrames.addLast(frame);
+ }
+
+ // Wake up this thread.
+ notifyAll();
+ }
+
+ // Queued.
+ return true;
+ }
+
+
+ private static boolean isHighPriorityFrame(WebSocketFrame frame) {
+ return (frame.isPingFrame() || frame.isPongFrame());
+ }
+
+
+ private void addHighPriorityFrame(WebSocketFrame frame) {
+ int index = 0;
+
+ // Determine the index at which the frame is added.
+ // Among high priority frames, the order is kept in insertion order.
+ for (WebSocketFrame f : mFrames) {
+ // If a non high-priority frame was found.
+ if (isHighPriorityFrame(f) == false) {
+ break;
+ }
+
+ ++index;
+ }
+
+ mFrames.add(index, frame);
+ }
+
+
+ public void queueFlush() {
+ synchronized (this) {
+ mFlushNeeded = true;
+
+ // Wake up this thread.
+ notifyAll();
+ }
+ }
+
+
+ private void flushIgnoreError() {
+ try {
+ flush();
+ } catch (IOException e) {
+ }
+ }
+
+
+ private void flush() throws IOException {
+ mWebSocket.getOutput().flush();
+ }
+
+
+ private int waitForFrames() {
+ synchronized (this) {
+ // If this thread has been requested to stop.
+ if (mStopRequested) {
+ return SHOULD_STOP;
+ }
+
+ // If a close frame has already been sent.
+ if (mCloseFrame != null) {
+ return SHOULD_STOP;
+ }
+
+ // If the list of web socket frames to be sent is empty.
+ if (mFrames.size() == 0) {
+ // Check mFlushNeeded before calling wait().
+ if (mFlushNeeded) {
+ mFlushNeeded = false;
+ return SHOULD_FLUSH;
+ }
+
+ try {
+ // Wait until a new frame is added to the list
+ // or this thread is requested to stop.
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ if (mStopRequested) {
+ return SHOULD_STOP;
+ }
+
+ if (mFrames.size() == 0) {
+ if (mFlushNeeded) {
+ mFlushNeeded = false;
+ return SHOULD_FLUSH;
+ }
+
+ // Spurious wakeup.
+ return SHOULD_CONTINUE;
+ }
+ }
+
+ return SHOULD_SEND;
+ }
+
+
+ private void sendFrames(boolean last) throws WebSocketException {
+ // The timestamp at which the last flush was executed.
+ long lastFlushAt = System.currentTimeMillis();
+
+ while (true) {
+ WebSocketFrame frame;
+
+ synchronized (this) {
+ // Pick up one frame from the queue.
+ frame = mFrames.poll();
+
+ // Mainly for queueFrame().
+ notifyAll();
+
+ // If the queue is empty.
+ if (frame == null) {
+ // No frame to process.
+ break;
+ }
+ }
+
+ // Send the frame to the server.
+ sendFrame(frame);
+
+ // If the frame is PING or PONG.
+ if (frame.isPingFrame() || frame.isPongFrame()) {
+ // Deliver the frame to the server immediately.
+ doFlush();
+ lastFlushAt = System.currentTimeMillis();
+ continue;
+ }
+
+ // If flush is not needed.
+ if (isFlushNeeded(last) == false) {
+ // Try to consume the next frame without flush.
+ continue;
+ }
+
+ // Flush if long time has passed since the last flush.
+ lastFlushAt = flushIfLongInterval(lastFlushAt);
+ }
+
+ if (isFlushNeeded(last)) {
+ doFlush();
+ }
+ }
+
+
+ private boolean isFlushNeeded(boolean last) {
+ return (last || mWebSocket.isAutoFlush() || mFlushNeeded || mCloseFrame != null);
+ }
+
+
+ private long flushIfLongInterval(long lastFlushAt) throws WebSocketException {
+ // The current timestamp.
+ long current = System.currentTimeMillis();
+
+ // If sending frames has taken too much time since the last flush.
+ if (FLUSH_THRESHOLD < (current - lastFlushAt)) {
+ // Flush without waiting for remaining frames to be processed.
+ doFlush();
+
+ // Update the timestamp at which the last flush was executed.
+ return current;
+ } else {
+ // Flush is not needed now.
+ return lastFlushAt;
+ }
+ }
+
+
+ private void doFlush() throws WebSocketException {
+ try {
+ // Flush
+ flush();
+
+ synchronized (this) {
+ mFlushNeeded = false;
+ }
+ } catch (IOException e) {
+ // Flushing frames to the server failed.
+ WebSocketException cause = new WebSocketException(WebSocketError.FLUSH_ERROR, "Flushing frames to the server failed: " + e.getMessage(), e);
+
+ // Notify the listeners.
+ ListenerManager manager = mWebSocket.getListenerManager();
+ manager.callOnError(cause);
+ manager.callOnSendError(cause, null);
+
+ throw cause;
+ }
+ }
+
+
+ private void sendFrame(WebSocketFrame frame) throws WebSocketException {
+ // Compress the frame if appropriate.
+ frame = WebSocketFrame.compressFrame(frame, mPMCE);
+
+ // Notify the listeners that the frame is about to be sent.
+ mWebSocket.getListenerManager().callOnSendingFrame(frame);
+
+ boolean unsent = false;
+
+ // If a close frame has already been sent.
+ if (mCloseFrame != null) {
+ // Frames should not be sent to the server.
+ unsent = true;
+ }
+ // If the frame is a close frame.
+ else if (frame.isCloseFrame()) {
+ mCloseFrame = frame;
+ }
+
+ if (unsent) {
+ // Notify the listeners that the frame was not sent.
+ mWebSocket.getListenerManager().callOnFrameUnsent(frame);
+ return;
+ }
+
+ // If the frame is a close frame.
+ if (frame.isCloseFrame()) {
+ // Change the state to closing if its current value is
+ // neither CLOSING nor CLOSED.
+ changeToClosing();
+ }
+
+ try {
+ // Send the frame to the server.
+ mWebSocket.getOutput().write(frame);
+ } catch (IOException e) {
+ // An I/O error occurred when a frame was tried to be sent.
+ WebSocketException cause = new WebSocketException(WebSocketError.IO_ERROR_IN_WRITING, "An I/O error occurred when a frame was tried to be sent: " + e.getMessage(), e);
+
+ // Notify the listeners.
+ ListenerManager manager = mWebSocket.getListenerManager();
+ manager.callOnError(cause);
+ manager.callOnSendError(cause, frame);
+
+ throw cause;
+ }
+
+ // Notify the listeners that the frame was sent.
+ mWebSocket.getListenerManager().callOnFrameSent(frame);
+ }
+
+
+ private void changeToClosing() {
+ StateManager manager = mWebSocket.getStateManager();
+
+ boolean stateChanged = false;
+
+ synchronized (manager) {
+ // The current state of the web socket.
+ WebSocketState state = manager.getState();
+
+ // If the current state is neither CLOSING nor CLOSED.
+ if (state != CLOSING && state != CLOSED) {
+ // Change the state to CLOSING.
+ manager.changeToClosing(CloseInitiator.CLIENT);
+
+ stateChanged = true;
+ }
+ }
+
+ if (stateChanged) {
+ // Notify the listeners of the state change.
+ mWebSocket.getListenerManager().callOnStateChanged(CLOSING);
+ }
+ }
+
+
+ private void notifyFinished() {
+ mWebSocket.onWritingThreadFinished(mCloseFrame);
+ }
+}
diff --git a/src/main/java/com/neovisionaries/ws/client/package-info.java b/src/main/java/com/neovisionaries/ws/client/package-info.java
new file mode 100644
index 0000000..ac23e0c
--- /dev/null
+++ b/src/main/java/com/neovisionaries/ws/client/package-info.java
@@ -0,0 +1,47 @@
+/**
+ * High-quality WebSocket client implementation in Java. This implementation
+ *
+ *
+ *
complies with RFC 6455 (The WebSocket Protocol),
+ *
works on Java SE 1.5+ and Android,
+ *
supports all the frame types (continuation, binary, text, close, ping and pong),
+ *
provides a method to send a fragmented frame in addition to methods for unfragmented frames,
+ *
provides a method to get the underlying raw socket of a WebSocket to configure it,
+ *