From f32c829f6597cb3827b3366918bc03825f24d28c Mon Sep 17 00:00:00 2001 From: Saeed Mozaffari Date: Wed, 13 Jun 2018 17:02:07 +0430 Subject: [PATCH] first commit --- .gitignore | 1 + build.gradle | 31 + proguard-rules.pro | 17 + .../ws/client/ExampleInstrumentedTest.java | 26 + src/main/AndroidManifest.xml | 14 + .../com/neovisionaries/ws/client/Address.java | 52 + .../com/neovisionaries/ws/client/Base64.java | 91 + .../neovisionaries/ws/client/ByteArray.java | 312 ++ .../ws/client/ConnectThread.java | 41 + .../neovisionaries/ws/client/Connectable.java | 41 + .../ws/client/CounterPayloadGenerator.java | 35 + .../ws/client/DeflateCompressor.java | 60 + .../ws/client/DeflateDecompressor.java | 223 ++ .../neovisionaries/ws/client/DeflateUtil.java | 401 +++ .../ws/client/DistinguishedNameParser.java | 402 +++ .../ws/client/FinishThread.java | 29 + .../ws/client/FixedDistanceHuffman.java | 50 + .../ws/client/FixedLiteralLengthHuffman.java | 72 + .../ws/client/FormatException.java | 26 + .../ws/client/HandshakeBuilder.java | 465 +++ .../ws/client/HandshakeReader.java | 506 +++ .../client/HostnameUnverifiedException.java | 87 + .../com/neovisionaries/ws/client/Huffman.java | 176 + .../ws/client/InsufficientDataException.java | 44 + .../ws/client/ListenerManager.java | 442 +++ .../com/neovisionaries/ws/client/Misc.java | 338 ++ .../ws/client/NoMoreFrameException.java | 27 + .../ws/client/OkHostnameVerifier.java | 246 ++ .../ws/client/OpeningHandshakeException.java | 96 + .../ws/client/PayloadGenerator.java | 41 + .../PerMessageCompressionExtension.java | 45 + .../ws/client/PerMessageDeflateExtension.java | 526 +++ .../ws/client/PeriodicalFrameSender.java | 185 + .../neovisionaries/ws/client/PingSender.java | 32 + .../neovisionaries/ws/client/PongSender.java | 32 + .../ws/client/ProxyHandshaker.java | 237 ++ .../ws/client/ProxySettings.java | 654 ++++ .../ws/client/ReadingThread.java | 1030 ++++++ .../ws/client/SocketConnector.java | 194 + .../ws/client/SocketFactorySettings.java | 79 + .../ws/client/StateManager.java | 63 + .../neovisionaries/ws/client/StatusLine.java | 126 + .../neovisionaries/ws/client/ThreadType.java | 51 + .../com/neovisionaries/ws/client/Token.java | 117 + .../neovisionaries/ws/client/WebSocket.java | 3169 +++++++++++++++++ .../ws/client/WebSocketAdapter.java | 167 + .../ws/client/WebSocketCloseCode.java | 172 + .../ws/client/WebSocketError.java | 453 +++ .../ws/client/WebSocketException.java | 56 + .../ws/client/WebSocketExtension.java | 304 ++ .../ws/client/WebSocketFactory.java | 603 ++++ .../ws/client/WebSocketFrame.java | 1025 ++++++ .../ws/client/WebSocketInputStream.java | 183 + .../ws/client/WebSocketListener.java | 571 +++ .../ws/client/WebSocketOpcode.java | 64 + .../ws/client/WebSocketOutputStream.java | 135 + .../ws/client/WebSocketState.java | 111 + .../ws/client/WebSocketThread.java | 60 + .../ws/client/WritingThread.java | 447 +++ .../ws/client/package-info.java | 47 + src/main/res/values/strings.xml | 3 + .../neovisionaries/ws/client/MiscTest.java | 203 ++ .../PerMessageDeflateExtensionTest.java | 192 + .../neovisionaries/ws/client/TokenTest.java | 153 + .../ws/client/WebSocketExtensionTest.java | 84 + .../ws/client/WebSocketFrameTest.java | 174 + websocket.iml | 135 + 67 files changed, 16274 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 proguard-rules.pro create mode 100644 src/androidTest/java/com/neovisionaries/ws/client/ExampleInstrumentedTest.java create mode 100644 src/main/AndroidManifest.xml create mode 100644 src/main/java/com/neovisionaries/ws/client/Address.java create mode 100644 src/main/java/com/neovisionaries/ws/client/Base64.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ByteArray.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ConnectThread.java create mode 100644 src/main/java/com/neovisionaries/ws/client/Connectable.java create mode 100644 src/main/java/com/neovisionaries/ws/client/CounterPayloadGenerator.java create mode 100644 src/main/java/com/neovisionaries/ws/client/DeflateCompressor.java create mode 100644 src/main/java/com/neovisionaries/ws/client/DeflateDecompressor.java create mode 100644 src/main/java/com/neovisionaries/ws/client/DeflateUtil.java create mode 100644 src/main/java/com/neovisionaries/ws/client/DistinguishedNameParser.java create mode 100644 src/main/java/com/neovisionaries/ws/client/FinishThread.java create mode 100644 src/main/java/com/neovisionaries/ws/client/FixedDistanceHuffman.java create mode 100644 src/main/java/com/neovisionaries/ws/client/FixedLiteralLengthHuffman.java create mode 100644 src/main/java/com/neovisionaries/ws/client/FormatException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/HandshakeBuilder.java create mode 100644 src/main/java/com/neovisionaries/ws/client/HandshakeReader.java create mode 100644 src/main/java/com/neovisionaries/ws/client/HostnameUnverifiedException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/Huffman.java create mode 100644 src/main/java/com/neovisionaries/ws/client/InsufficientDataException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ListenerManager.java create mode 100644 src/main/java/com/neovisionaries/ws/client/Misc.java create mode 100644 src/main/java/com/neovisionaries/ws/client/NoMoreFrameException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/OkHostnameVerifier.java create mode 100644 src/main/java/com/neovisionaries/ws/client/OpeningHandshakeException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PayloadGenerator.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PerMessageCompressionExtension.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PerMessageDeflateExtension.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PeriodicalFrameSender.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PingSender.java create mode 100644 src/main/java/com/neovisionaries/ws/client/PongSender.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ProxyHandshaker.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ProxySettings.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ReadingThread.java create mode 100644 src/main/java/com/neovisionaries/ws/client/SocketConnector.java create mode 100644 src/main/java/com/neovisionaries/ws/client/SocketFactorySettings.java create mode 100644 src/main/java/com/neovisionaries/ws/client/StateManager.java create mode 100644 src/main/java/com/neovisionaries/ws/client/StatusLine.java create mode 100644 src/main/java/com/neovisionaries/ws/client/ThreadType.java create mode 100644 src/main/java/com/neovisionaries/ws/client/Token.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocket.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketAdapter.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketCloseCode.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketError.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketException.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketExtension.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketFactory.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketFrame.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketInputStream.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketListener.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketOpcode.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketOutputStream.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketState.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WebSocketThread.java create mode 100644 src/main/java/com/neovisionaries/ws/client/WritingThread.java create mode 100644 src/main/java/com/neovisionaries/ws/client/package-info.java create mode 100644 src/main/res/values/strings.xml create mode 100644 src/test/java/com/neovisionaries/ws/client/MiscTest.java create mode 100644 src/test/java/com/neovisionaries/ws/client/PerMessageDeflateExtensionTest.java create mode 100644 src/test/java/com/neovisionaries/ws/client/TokenTest.java create mode 100644 src/test/java/com/neovisionaries/ws/client/WebSocketExtensionTest.java create mode 100644 src/test/java/com/neovisionaries/ws/client/WebSocketFrameTest.java create mode 100644 websocket.iml 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}. + *

+ * + *

+ * See Verify that certificate is valid for server hostname (#107). + *

+ * + * @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. + *

+ * + *
+ *
    + *
  1. + * If {@link #isSecure()} returns {@code true}, + *
      + *
    1. + * 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. + *
    2. + * Otherwise, if an {@link SSLSocketFactory} instance has been + * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the + * instance is used. + *
    3. + * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()} + * is used. + *
    + *
  2. + * Otherwise (= {@link #isSecure()} returns {@code false}), + *
      + *
    1. + * If a {@link SocketFactory} instance has been set by {@link + * #setSocketFactory(SocketFactory)}, the instance is used. + *
    2. + * 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. + * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameValueDescription
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.
HeadersClearedAdditional 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. + *

+ * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
{@code WebSocketListener} methods
MethodDescription
{@link WebSocketListener#handleCallbackError(WebSocket, Throwable) handleCallbackError}Called when an onXxx() method threw a {@code Throwable}.
{@link WebSocketListener#onBinaryFrame(WebSocket, WebSocketFrame) onBinaryFrame}Called when a binary frame was received.
{@link WebSocketListener#onBinaryMessage(WebSocket, byte[]) onBinaryMessage}Called when a binary message was received.
{@link WebSocketListener#onCloseFrame(WebSocket, WebSocketFrame) onCloseFrame}Called when a close frame was received.
{@link WebSocketListener#onConnected(WebSocket, Map) onConnected}Called after the opening handshake succeeded.
{@link WebSocketListener#onConnectError(WebSocket, WebSocketException) onConnectError}Called when {@link #connectAsynchronously()} failed.
{@link WebSocketListener#onContinuationFrame(WebSocket, WebSocketFrame) onContinuationFrame}Called when a continuation frame was received.
{@link WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame, boolean) onDisconnected}Called after a WebSocket connection was closed.
{@link WebSocketListener#onError(WebSocket, WebSocketException) onError}Called when an error occurred.
{@link WebSocketListener#onFrame(WebSocket, WebSocketFrame) onFrame}Called when a frame was received.
{@link WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame) onFrameError}Called when a frame failed to be read.
{@link WebSocketListener#onFrameSent(WebSocket, WebSocketFrame) onFrameSent}Called when a frame was sent.
{@link WebSocketListener#onFrameUnsent(WebSocket, WebSocketFrame) onFrameUnsent}Called when a frame was not sent.
{@link WebSocketListener#onMessageDecompressionError(WebSocket, WebSocketException, byte[]) onMessageDecompressionError}Called when a message failed to be decompressed.
{@link WebSocketListener#onMessageError(WebSocket, WebSocketException, List) onMessageError}Called when a message failed to be constructed.
{@link WebSocketListener#onPingFrame(WebSocket, WebSocketFrame) onPingFrame}Called when a ping frame was received.
{@link WebSocketListener#onPongFrame(WebSocket, WebSocketFrame) onPongFrame}Called when a pong frame was received.
{@link WebSocketListener#onSendError(WebSocket, WebSocketException, WebSocketFrame) onSendError}Called when an error occurred on sending a frame.
{@link WebSocketListener#onSendingFrame(WebSocket, WebSocketFrame) onSendingFrame}Called before a frame is sent.
{@link WebSocketListener#onSendingHandshake(WebSocket, String, List) onSendingHandshake}Called before an opening handshake is sent.
{@link WebSocketListener#onStateChanged(WebSocket, WebSocketState) onStateChanged}Called when the state of WebSocket changed.
{@link WebSocketListener#onTextFrame(WebSocket, WebSocketFrame) onTextFrame}Called when a text frame was received.
{@link WebSocketListener#onTextMessage(WebSocket, String) onTextMessage}Called when a text message was received.
{@link WebSocketListener#onTextMessageError(WebSocket, WebSocketException, byte[]) onTextMessageError}Called when a text message failed to be constructed.
{@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread) onThreadCreated}Called after a thread was created.
{@link WebSocketListener#onThreadStarted(WebSocket, ThreadType, Thread) onThreadStarted}Called at the beginning of a thread's {@code run()} method. + *
{@link WebSocketListener#onThreadStopping(WebSocket, ThreadType, Thread) onThreadStopping}Called at the end of a thread's {@code run()} method. + *
{@link WebSocketListener#onUnexpectedError(WebSocket, WebSocketException) onUnexpectedError}Called when an uncaught throwable was detected.
+ *
+ * + *

Configure WebSocket

+ * + *

+ * Before starting a WebSocket opening handshake with the server, you can configure the + * {@code WebSocket} instance by using the following methods. + *

+ * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Methods for Configuration
METHODDESCRIPTION
{@link #addProtocol(String) addProtocol}Adds an element to {@code Sec-WebSocket-Protocol}
{@link #addExtension(WebSocketExtension) addExtension}Adds an element to {@code Sec-WebSocket-Extensions}
{@link #addHeader(String, String) addHeader}Adds an arbitrary HTTP header.
{@link #setUserInfo(String, String) setUserInfo}Adds {@code Authorization} header for Basic Authentication.
{@link #getSocket() getSocket}Gets the underlying {@link Socket} instance to configure it.
{@link #setExtended(boolean) setExtended}Disables validity checks on RSV1/RSV2/RSV3 and opcode.
{@link #setFrameQueueSize(int) setFrameQueueSize}Set the size of the frame queue for congestion control.
{@link #setMaxPayloadSize(int) setMaxPayloadSize}Set the maximum payload size.
{@link #setMissingCloseFrameAllowed(boolean) setMissingCloseFrameAllowed}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. + *

+ * + *
+ *
 catch ({@link OpeningHandshakeException} e)
+ * {
+ * // Status line.
+ * {@link StatusLine} sl = e.{@link OpeningHandshakeException#getStatusLine() getStatusLine()};
+ * System.out.println("=== Status Line ===");
+ * System.out.format("HTTP Version  = %s\n", sl.{@link StatusLine#getHttpVersion() getHttpVersion()});
+ * System.out.format("Status Code   = %d\n", sl.{@link StatusLine#getStatusCode() getStatusCode()});
+ * System.out.format("Reason Phrase = %s\n", sl.{@link StatusLine#getReasonPhrase() getReasonPhrase()});
+ *
+ * // HTTP headers.
+ * Map<String, List<String>> headers = e.{@link OpeningHandshakeException#getHeaders() getHeaders()};
+ * System.out.println("=== HTTP Headers ===");
+ * for (Map.Entry<String, List<String>> entry : headers.entrySet())
+ * {
+ * // Header name.
+ * String name = entry.getKey();
+ *
+ * // Values of the header.
+ * List<String> values = entry.getValue();
+ *
+ * if (values == null || values.size() == 0)
+ * {
+ * // Print the name only.
+ * System.out.println(name);
+ * continue;
+ * }
+ *
+ * for (String value : values)
+ * {
+ * // Print the name and the value.
+ * System.out.format("%s: %s\n", name, value);
+ * }
+ * }
+ * }
+ *
+ * + *

+ * 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)}. + *

+ * + *
+ *
 // Disable auto-flush.
+ * ws.{@link #setAutoFlush(boolean) setAutoFlush}(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"}. + *

+ * + *
+ *
 // Enable "permessage-deflate" extension (RFC 7692).
+ * ws.{@link #addExtension(String) addExtension}({@link WebSocketExtension#PERMESSAGE_DEFLATE});
+ *
+ * + *

Missing Close Frame

+ * + *

+ * 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}. + *

+ * + *
+ *
 {@code @}Override
+ * public void {@link WebSocketListener#handleCallbackError(WebSocket, Throwable)
+ * handleCallbackError}(WebSocket websocket, Throwable cause) throws Exception {
+ * // Throwables thrown by onXxx() callback methods come here.
+ * }
+ *
+ * + *

Thread Callbacks

+ * + *

+ * Some threads are created internally in the implementation of {@code WebSocket}. + * Known threads are as follows. + *

+ * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Internal Threads
THREAD TYPEDESCRIPTION
{@link ThreadType#READING_THREAD READING_THREAD}A thread which reads WebSocket frames from the server.
{@link ThreadType#WRITING_THREAD WRITING_THREAD}A thread which sends WebSocket frames to the server.
{@link ThreadType#CONNECT_THREAD CONNECT_THREAD}A thread which calls {@link WebSocket#connect()} asynchronously.
{@link ThreadType#FINISH_THREAD FINISH_THREAD}A thread which does finalization of a {@code WebSocket} instance.
+ *
+ * + *

+ * The following callback methods of {@link WebSocketListener} are called according + * to the life cycle of the threads. + *

+ * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Thread Callbacks
METHODDESCRIPTION
{@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread) onThreadCreated()}Called after a thread was created.
{@link WebSocketListener#onThreadStarted(WebSocket, ThreadType, Thread) onThreadStarted()}Called at the beginning of the thread's {@code run()} method.
{@link WebSocketListener#onThreadStopping(WebSocket, ThreadType, Thread) onThreadStopping()}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. + *

+ * + *
+ *
 {@code @}Override
+ * public void {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread)
+ * onThreadCreated}(WebSocket websocket, {@link ThreadType} type, Thread thread)
+ * {
+ * if (type == ThreadType.READING_THREAD)
+ * {
+ * thread.setName("READING_THREAD");
+ * }
+ * }
+ *
+ * + * @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. + *

+ * + *
+ *
+ *
+ * An excerpt from RFC 6455, 5.5.3. Pong + *
+ *
+ *

+ * 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. + *

+ * + *
+     * WebSocket websocket = ......;
+     * websocket.{@link #getSocket() getSocket()}.{@link Socket#setSoTimeout(int)
+     * setSoTimeout}(5000);
+     * 
+ * + *

+ * 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.) + *

+ * + * @return {@code this} object. + * @throws WebSocketException
    + *
  • 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. + * + *
+ * executorService.{@link ExecutorService#submit(Callable) submit}({@link #connectable()}) + *
+ * + * @param executorService An {@link ExecutorService} to execute a task created by + * {@link #connectable()}. + * @return The value returned from {@link ExecutorService#submit(Callable)}. + * @throws NullPointerException If the given {@link ExecutorService} is {@code null}. + * @throws RejectedExecutionException If the given {@link ExecutorService} rejected the task + * created by {@link #connectable()}. + * @see #connectAsynchronously() + * @since 1.7 + */ + public Future connect(ExecutorService executorService) { + return executorService.submit(connectable()); + } + + + /** + * Get a new {@link Callable}{@code <}{@link WebSocket}{@code >} instance + * whose {@link Callable#call() call()} method calls {@link #connect()} + * method of this {@code WebSocket} instance. + * + * @return A new {@link Callable}{@code <}{@link WebSocket}{@code >} instance + * for asynchronous {@link #connect()}. + * @see #connect(ExecutorService) + * @since 1.7 + */ + public Callable connectable() { + return new Connectable(this); + } + + + /** + * Execute {@link #connect()} asynchronously by creating a new thread and + * calling {@code connect()} in the thread. If {@code connect()} failed, + * {@link WebSocketListener#onConnectError(WebSocket, WebSocketException) + * onConnectError()} method of {@link WebSocketListener} is called. + * + * @return {@code this} object. + * @since 1.8 + */ + public WebSocket connectAsynchronously() { + Thread thread = new ConnectThread(this); + + // Get the reference (just in case) + ListenerManager lm = mListenerManager; + + if (lm != null) { + lm.callOnThreadCreated(ThreadType.CONNECT_THREAD, thread); + } + + thread.start(); + + return this; + } + + + /** + * Disconnect the WebSocket. + * + *

+ * This method is an alias of {@link #disconnect(int, String) + * disconnect}{@code (}{@link WebSocketCloseCode#NORMAL}{@code , null)}. + *

+ * + * @return {@code this} object. + */ + public WebSocket disconnect() { + return disconnect(WebSocketCloseCode.NORMAL, null); + } + + + /** + * Disconnect the WebSocket. + * + *

+ * 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. + * + *
+ *

From RFC 6455, 5.2 Base Framing Protocol; RSV1, RSV2, RSV3:

+ *

+ * 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. + * + *
+ *

From RFC 6455, 5.2 Base Framing Protocol; RSV1, RSV2, RSV3:

+ *

+ * 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. + * + *
+ *

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. + *

+ *
+ */ + FRAME_MASKED, + + + /** + * A frame has an unknown opcode. + * + *

+ * By calling {@link WebSocket#setExtended(boolean) WebSocket.setExtended}{@code + * (true)}, you can accept frames which have an unknown opcode. + *

+ */ + UNKNOWN_OPCODE, + + + /** + * A control frame is fragmented. + * + *
+ *

From RFC 6455, 5.4. Fragmentation:

+ *

+ * 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). + * + *
+ *

From RFC 6455, 5.5. Control Frames:

+ *

+ * All control frames MUST have a payload length of 125 bytes or less and + * MUST NOT be fragmented. + *

+ *
+ */ + TOO_LONG_CONTROL_FRAME_PAYLOAD, + + + /** + * Failed to concatenate payloads of multiple frames to construct a message. + */ + MESSAGE_CONSTRUCTION_ERROR, + + + /** + * Failed to convert payload data into a string. + */ + TEXT_MESSAGE_CONSTRUCTION_ERROR, + + + /** + * An uncaught throwable was detected in the reading thread (which reads + * frames from the server). + */ + UNEXPECTED_ERROR_IN_READING_THREAD, + + + /** + * An uncaught throwable was detected in the writing thread (which sends + * frames to the server). + */ + UNEXPECTED_ERROR_IN_WRITING_THREAD, + + + /** + * {@code permessage-deflate} extension contains an unsupported parameter. + * + *

+ * See 7. The + * "permessage-deflate" Extension in + * RFC 7692 for details. + *

+ * + * @since 1.15 + */ + PERMESSAGE_DEFLATE_UNSUPPORTED_PARAMETER, + + + /** + * The value of {@code server_max_window_bits} parameter or {@code + * client_max_window_bits} parameter of {@code permessage-deflate} + * extension is invalid. + * + *

+ * See 7.1.2. + * Limiting the LZ77 Sliding Window Size in + * RFC 7692 for details. + *

+ * + * @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 + *

+ * + *

+ * See Verify that certificate is valid for server hostname (#107). + *

+ * + * @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. + *

+ * + *
    + *
  1. + * If the scheme of the URI is either {@code wss} or {@code https}, + *
      + *
    1. + * 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. + *
    2. + * Otherwise, if an {@link SSLSocketFactory} instance has been + * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the + * instance is used. + *
    3. + * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()} + * is used. + *
    + *
  2. + * Otherwise (= the scheme of the URI is either {@code ws} or {@code http}), + *
      + *
    1. + * If a {@link SocketFactory} instance has been set by {@link + * #setSocketFactory(SocketFactory)}, the instance is used. + *
    2. + * 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. + *

+ * + *
    + *
  1. + * If the scheme of the URI is either {@code wss} or {@code https}, + *
      + *
    1. + * 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. + *
    2. + * Otherwise, if an {@link SSLSocketFactory} instance has been + * set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the + * instance is used. + *
    3. + * Otherwise, the value returned from {@link SSLSocketFactory#getDefault()} + * is used. + *
    + *
  2. + * Otherwise (= the scheme of the URI is either {@code ws} or {@code http}), + *
      + *
    1. + * If a {@link SocketFactory} instance has been set by {@link + * #setSocketFactory(SocketFactory)}, the instance is used. + *
    2. + * 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
ValueDescription
0x0Frame continuation
0x1Text frame
0x2Binary frame
0x3-0x7Reserved
0x8Connection close
0x9Ping
0xAPong
0xB-0xFReserved
+ * + * @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. + * + *

+ * The logic of masking/unmasking is described in "5.3. + * Client-to-Server Masking" in RFC 6455. + *

+ * + * @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, + *
  • provides a method for Basic Authentication, + *
  • provides a factory class which utilizes {@link javax.net.SocketFactory} interface, + *
  • provides a rich listener interface to hook WebSocket events, + *
  • has fine-grained error codes for fine-grained controllability on errors, + *
  • allows to disable validity checks on RSV1/RSV2/RSV3 bits and opcode of frames, + *
  • supports HTTP proxy, especially "Secure WebSocket" (wss) through + * "Secure Proxy" (https), + *
  • and supports RFC 7692 + * (Compression Extensions for WebSocket), also known as permessage-deflate + * (not enabled by default). + *
+ * + *

+ * See the description of {@link com.neovisionaries.ws.client.WebSocket WebSocket} + * class for usage. The source code is hosted at + * GitHub. + *

+ * + *

+ * For Maven: + *

+ *
+ * + *
+ * <dependency>
+ *     <groupId>com.neovisionaries</groupId>
+ *     <artifactId>nv-websocket-client</artifactId>
+ *     <version>2.2</version>
+ * </dependency>
+ *
+ * + * @version 2.2 + * + * @author Takahiko Kawasaki + */ +package com.neovisionaries.ws.client; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c0526d --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + websocket + diff --git a/src/test/java/com/neovisionaries/ws/client/MiscTest.java b/src/test/java/com/neovisionaries/ws/client/MiscTest.java new file mode 100644 index 0000000..6df6ff4 --- /dev/null +++ b/src/test/java/com/neovisionaries/ws/client/MiscTest.java @@ -0,0 +1,203 @@ +/* + * 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.URI; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +public class MiscTest { + private static void extractHostTest(String expected, String input) { + URI uri = URI.create(input); + + String result = Misc.extractHost(uri); + + assertEquals(expected, result); + } + + + private static void extractHostFromAuthorityPartTest(String expected, String input) { + String result = Misc.extractHostFromAuthorityPart(input); + + assertEquals(expected, result); + } + + + private static void extractHostFromEntireUriTest(String expected, String input) { + String result = Misc.extractHostFromEntireUri(input); + + assertEquals(expected, result); + } + + + @Test + public void test01() { + extractHostFromAuthorityPartTest("example.com", "example.com"); + } + + + @Test + public void test02() { + extractHostFromAuthorityPartTest("example.com", "example.com:8080"); + } + + + @Test + public void test03() { + extractHostFromAuthorityPartTest("example.com", "id:password@example.com"); + } + + + @Test + public void test04() { + extractHostFromAuthorityPartTest("example.com", "id:password@example.com:8080"); + } + + + @Test + public void test05() { + extractHostFromAuthorityPartTest("example.com", "id@example.com"); + } + + + @Test + public void test06() { + extractHostFromAuthorityPartTest("example.com", "id:@example.com"); + } + + + @Test + public void test07() { + extractHostFromAuthorityPartTest("example.com", ":@example.com"); + } + + + @Test + public void test08() { + extractHostFromAuthorityPartTest("example.com", ":password@example.com"); + } + + + @Test + public void test09() { + extractHostFromAuthorityPartTest("example.com", "@example.com"); + } + + + @Test + public void test10() { + extractHostFromEntireUriTest("example.com", "ws://example.com"); + } + + + @Test + public void test11() { + extractHostFromEntireUriTest("example.com", "ws://example.com:8080"); + } + + + @Test + public void test12() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com"); + } + + + @Test + public void test13() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com:8080"); + } + + + @Test + public void test14() { + extractHostFromEntireUriTest("example.com", "ws://example.com/"); + } + + + @Test + public void test15() { + extractHostFromEntireUriTest("example.com", "ws://example.com:8080/"); + } + + + @Test + public void test16() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com/"); + } + + + @Test + public void test17() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com:8080/"); + } + + + @Test + public void test18() { + extractHostFromEntireUriTest("example.com", "ws://example.com/path?key=@value"); + } + + + @Test + public void test19() { + extractHostFromEntireUriTest("example.com", "ws://example.com:8080/path?key=@value"); + } + + + @Test + public void test20() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com/path?key=@value"); + } + + + @Test + public void test21() { + extractHostFromEntireUriTest("example.com", "ws://id:password@example.com:8080/path?key=@value"); + } + + + @Test + public void test22() { + extractHostTest("example.com", "ws://example.com"); + } + + + @Test + public void test23() { + extractHostTest("example.com", "ws://example.com:8080"); + } + + + @Test + public void test24() { + extractHostTest("example.com", "ws://id:password@example.com"); + } + + + @Test + public void test25() { + extractHostTest("example.com", "ws://id:password@example.com:8080"); + } + + + @Test + public void test26() { + extractHostTest("example.com", "ws://id:password@example.com:8080/path?key=@value"); + } +} diff --git a/src/test/java/com/neovisionaries/ws/client/PerMessageDeflateExtensionTest.java b/src/test/java/com/neovisionaries/ws/client/PerMessageDeflateExtensionTest.java new file mode 100644 index 0000000..6928f08 --- /dev/null +++ b/src/test/java/com/neovisionaries/ws/client/PerMessageDeflateExtensionTest.java @@ -0,0 +1,192 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +public class PerMessageDeflateExtensionTest { + private static final int DEFAULT_WINDOW_SIZE = 32768; + + + private static PerMessageDeflateExtension parse(String text) { + return (PerMessageDeflateExtension) WebSocketExtension.parse(text); + } + + + private static PerMessageDeflateExtension parseValid(String text) { + PerMessageDeflateExtension extension = parse(text); + + try { + extension.validate(); + } catch (WebSocketException e) { + return null; + } + + return extension; + } + + + private static WebSocketException parseInvalid(String text) { + PerMessageDeflateExtension extension = parse(text); + + try { + extension.validate(); + } catch (WebSocketException e) { + return e; + } + + return null; + } + + + @Test + public void test001() { + PerMessageDeflateExtension extension = parseValid("permessage-deflate"); + + assertNotNull(extension); + assertFalse(extension.isServerNoContextTakeover()); + assertFalse(extension.isClientNoContextTakeover()); + assertEquals(DEFAULT_WINDOW_SIZE, extension.getServerWindowSize()); + assertEquals(DEFAULT_WINDOW_SIZE, extension.getClientWindowSize()); + } + + + @Test + public void test002() { + PerMessageDeflateExtension extension = parseValid("permessage-deflate; server_no_context_takeover; client_no_context_takeover"); + + assertNotNull(extension); + assertTrue(extension.isServerNoContextTakeover()); + assertTrue(extension.isClientNoContextTakeover()); + } + + + @Test + public void test003() { + PerMessageDeflateExtension extension = parseValid("permessage-deflate; server_max_window_bits=8; client_max_window_bits=8"); + + assertNotNull(extension); + assertEquals(256, extension.getServerWindowSize()); + assertEquals(256, extension.getClientWindowSize()); + } + + + @Test + public void test004() { + WebSocketException exception = parseInvalid("permessage-deflate; unknown_parameter"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_UNSUPPORTED_PARAMETER, exception.getError()); + } + + + @Test + public void test005() { + WebSocketException exception = parseInvalid("permessage-deflate; server_max_window_bits"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test006() { + WebSocketException exception = parseInvalid("permessage-deflate; server_max_window_bits=abc"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test007() { + WebSocketException exception = parseInvalid("permessage-deflate; server_max_window_bits=0"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test008() { + WebSocketException exception = parseInvalid("permessage-deflate; server_max_window_bits=7"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test009() { + WebSocketException exception = parseInvalid("permessage-deflate; server_max_window_bits=16"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test010() { + WebSocketException exception = parseInvalid("permessage-deflate; client_max_window_bits"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test011() { + WebSocketException exception = parseInvalid("permessage-deflate; client_max_window_bits=abc"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test012() { + WebSocketException exception = parseInvalid("permessage-deflate; client_max_window_bits=0"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test013() { + WebSocketException exception = parseInvalid("permessage-deflate; client_max_window_bits=7"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } + + + @Test + public void test014() { + WebSocketException exception = parseInvalid("permessage-deflate; client_max_window_bits=16"); + + assertNotNull(exception); + assertSame(WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, exception.getError()); + } +} diff --git a/src/test/java/com/neovisionaries/ws/client/TokenTest.java b/src/test/java/com/neovisionaries/ws/client/TokenTest.java new file mode 100644 index 0000000..3c4321f --- /dev/null +++ b/src/test/java/com/neovisionaries/ws/client/TokenTest.java @@ -0,0 +1,153 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +public class TokenTest { + private static void isValid(String text) { + assertTrue(Token.isValid(text)); + } + + + private static void isInvalid(String text) { + assertFalse(Token.isValid(text)); + } + + + private static void unescape(String expected, String input) { + assertEquals(expected, Token.unescape(input)); + } + + + private static void unquote(String expected, String input) { + assertEquals(expected, Token.unquote(input)); + } + + + @Test + public void test001() { + isInvalid(null); + } + + + @Test + public void test002() { + isInvalid(""); + } + + + @Test + public void test003() { + isInvalid(" "); + } + + + @Test + public void test004() { + isValid("abc"); + } + + + @Test + public void test005() { + unescape(null, null); + } + + + @Test + public void test006() { + unescape("", ""); + } + + + @Test + public void test007() { + unescape("abc", "abc"); + } + + + @Test + public void test008() { + unescape("abc", "ab\\c"); + } + + + @Test + public void test009() { + unescape("ab\\", "ab\\\\"); + } + + + @Test + public void test010() { + unescape("ab\\c", "ab\\\\c"); + } + + + @Test + public void test011() { + unquote(null, null); + } + + + @Test + public void test012() { + unquote("", ""); + } + + + @Test + public void test013() { + unquote("abc", "abc"); + } + + + @Test + public void test014() { + unquote("abc", "\"abc\""); + } + + + @Test + public void test015() { + unquote("\"abc", "\"abc"); + } + + + @Test + public void test016() { + unquote("abc\"", "abc\""); + } + + + @Test + public void test017() { + unquote("abc", "\"ab\\c\""); + } + + + @Test + public void test018() { + unquote("ab\\c", "\"ab\\\\c\""); + } +} diff --git a/src/test/java/com/neovisionaries/ws/client/WebSocketExtensionTest.java b/src/test/java/com/neovisionaries/ws/client/WebSocketExtensionTest.java new file mode 100644 index 0000000..2aafab9 --- /dev/null +++ b/src/test/java/com/neovisionaries/ws/client/WebSocketExtensionTest.java @@ -0,0 +1,84 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + + +public class WebSocketExtensionTest { + private static WebSocketExtension parse(String text) { + return WebSocketExtension.parse(text); + } + + + @Test + public void test001() { + WebSocketExtension extension = parse("abc"); + + assertNotNull(extension); + assertEquals("abc", extension.getName()); + } + + + @Test + public void test002() { + WebSocketExtension extension = parse("abc; x=1; y=2"); + + assertNotNull(extension); + assertEquals("abc", extension.getName()); + assertEquals("1", extension.getParameter("x")); + assertEquals("2", extension.getParameter("y")); + } + + + @Test + public void test003() { + WebSocketExtension extension = parse("abc; x"); + + assertNotNull(extension); + assertEquals("abc", extension.getName()); + assertNull(extension.getParameter("x")); + assertTrue(extension.containsParameter("x")); + } + + + @Test + public void test004() { + WebSocketExtension extension = parse("abc; x="); + + assertNotNull(extension); + assertEquals("abc", extension.getName()); + assertFalse(extension.containsParameter("x")); + } + + + @Test + public void test005() { + WebSocketExtension extension = parse("abc; x=\"1\"; y=\"2\""); + + assertNotNull(extension); + assertEquals("abc", extension.getName()); + assertEquals("1", extension.getParameter("x")); + assertEquals("2", extension.getParameter("y")); + } +} diff --git a/src/test/java/com/neovisionaries/ws/client/WebSocketFrameTest.java b/src/test/java/com/neovisionaries/ws/client/WebSocketFrameTest.java new file mode 100644 index 0000000..60e5e63 --- /dev/null +++ b/src/test/java/com/neovisionaries/ws/client/WebSocketFrameTest.java @@ -0,0 +1,174 @@ +/* + * 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.List; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + + +public class WebSocketFrameTest { + @Test + public void test001() { + WebSocketFrame frame = WebSocketFrame.createTextFrame(null); + + assertTrue(frame.toString().endsWith("Payload=null)")); + } + + + @Test + public void test002() { + WebSocketFrame frame = WebSocketFrame.createTextFrame("dummy"); + frame.setRsv1(true); + + assertTrue(frame.toString().endsWith("Payload=compressed)")); + } + + + @Test + public void test003() { + WebSocketFrame frame = WebSocketFrame.createTextFrame("hello"); + + assertTrue(frame.toString().endsWith("Payload=\"hello\")")); + } + + + @Test + public void test004() { + WebSocketFrame frame = WebSocketFrame.createBinaryFrame(null); + + assertTrue(frame.toString().endsWith("Payload=null)")); + } + + + @Test + public void test005() { + byte[] payload = new byte[]{(byte) 0x01, (byte) 0x23, (byte) 0xAB}; + WebSocketFrame frame = WebSocketFrame.createBinaryFrame(payload); + frame.setRsv1(true); + + assertTrue(frame.toString().endsWith("Payload=compressed)")); + } + + + @Test + public void test006() { + byte[] payload = new byte[]{(byte) 0x01, (byte) 0x23, (byte) 0xAB}; + WebSocketFrame frame = WebSocketFrame.createBinaryFrame(payload); + + assertTrue(frame.toString().endsWith("Payload=01 23 AB)")); + } + + + @Test + public void test007() { + WebSocketFrame frame = WebSocketFrame.createTextFrame("0123456789"); + List list = WebSocketFrame.splitIfNecessary(frame, 3, null); + + assertNotNull(list); + assertEquals(4, list.size()); + + frame = list.get(0); + assertEquals("012", frame.getPayloadText()); + assertEquals(true, frame.isTextFrame()); + assertEquals(false, frame.getFin()); + + frame = list.get(1); + assertEquals("345", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(false, frame.getFin()); + + frame = list.get(2); + assertEquals("678", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(false, frame.getFin()); + + frame = list.get(3); + assertEquals("9", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(true, frame.getFin()); + } + + + @Test + public void test008() { + WebSocketFrame frame = WebSocketFrame.createContinuationFrame("ABCDEF"); + List list = WebSocketFrame.splitIfNecessary(frame, 2, null); + + assertNotNull(list); + assertEquals(3, list.size()); + + frame = list.get(0); + assertEquals("AB", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(false, frame.getFin()); + + frame = list.get(1); + assertEquals("CD", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(false, frame.getFin()); + + frame = list.get(2); + assertEquals("EF", frame.getPayloadText()); + assertEquals(true, frame.isContinuationFrame()); + assertEquals(false, frame.getFin()); + } + + + @Test + public void test009() { + String payload = "000000000000000000000000000000"; + WebSocketFrame frame = WebSocketFrame.createTextFrame(payload); + PerMessageCompressionExtension pmce = new PerMessageDeflateExtension(); + + // splitIfNecessary() compresses the WebSocket frame and does not split it. + List list = WebSocketFrame.splitIfNecessary(frame, payload.length() - 1, pmce); + + assertNull(list); + + // splitIfNecessary() compresses the WebSocket frame and + list = WebSocketFrame.splitIfNecessary(frame, 1, pmce); + assertNotNull(list); + } + + + @Test + public void test010() { + String payload = "000000000000000000000000000000111111111111111111111111111111"; + WebSocketFrame frame = WebSocketFrame.createTextFrame(payload); + PerMessageCompressionExtension pmce = new PerMessageDeflateExtension(); + + // splitIfNecessary() compresses the WebSocket frame and splits it. + List list = WebSocketFrame.splitIfNecessary(frame, 1, pmce); + + assertNotNull(list); + + // Compute the total payload length of the WebSocket frames. + int totalLength = 0; + for (WebSocketFrame f : list) { + totalLength += f.getPayloadLength(); + } + + // The total payload length of the WebSocket frames should be less + // than the payload length of the original WebSocket frame. + assertTrue(totalLength < payload.length()); + } +} diff --git a/websocket.iml b/websocket.iml new file mode 100644 index 0000000..d5fc302 --- /dev/null +++ b/websocket.iml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file