├── .gitignore ├── README.md ├── build.gradle └── src └── main └── java └── com └── codebutler └── android_websockets ├── HybiParser.java └── WebSocketClient.java /.gitignore: -------------------------------------------------------------------------------- 1 | gen/ 2 | build/ 3 | local.properties 4 | .gradle 5 | .idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket client for Android 2 | 3 | A very simple bare-minimum WebSocket client for Android. 4 | 5 | ## Credits 6 | 7 | The hybi parser is based on code from the [faye project](https://github.com/faye/faye-websocket-node). Faye is Copyright (c) 2009-2012 James Coglan. Many thanks for the great open-source library! 8 | 9 | Ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) . 10 | 11 | ## Usage 12 | 13 | Here's the entire API: 14 | 15 | ```java 16 | List extraHeaders = Arrays.asList( 17 | new BasicNameValuePair("Cookie", "session=abcd"); 18 | ); 19 | 20 | WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Handler() { 21 | @Override 22 | public void onConnect() { 23 | Log.d(TAG, "Connected!"); 24 | } 25 | 26 | @Override 27 | public void onMessage(String message) { 28 | Log.d(TAG, String.format("Got string message! %s", message)); 29 | } 30 | 31 | @Override 32 | public void onMessage(byte[] data) { 33 | Log.d(TAG, String.format("Got binary message! %s", toHexString(data)); 34 | } 35 | 36 | @Override 37 | public void onDisconnect(int code, String reason) { 38 | Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason)); 39 | } 40 | 41 | @Override 42 | public void onError(Exception error) { 43 | Log.e(TAG, "Error!", error); 44 | } 45 | }, extraHeaders); 46 | 47 | client.connect(); 48 | 49 | // Later… 50 | client.send("hello!"); 51 | client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }); 52 | client.disconnect(); 53 | ``` 54 | 55 | 56 | ## TODO 57 | 58 | * Run [autobahn tests](http://autobahn.ws/testsuite) 59 | * Investigate using [naga](http://code.google.com/p/naga/) instead of threads. 60 | 61 | ## License 62 | 63 | (The MIT License) 64 | 65 | Copyright (c) 2009-2012 James Coglan 66 | Copyright (c) 2012 Eric Butler 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of 69 | this software and associated documentation files (the 'Software'), to deal in 70 | the Software without restriction, including without limitation the rights to use, 71 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 72 | Software, and to permit persons to whom the Software is furnished to do so, 73 | subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 80 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 81 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 82 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 83 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 84 | 85 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | def android_jar = new File(System.getenv('ANDROID_HOME'), '/platforms/android-17/android.jar') 2 | 3 | apply plugin: 'java' 4 | 5 | sourceCompatibility = 1.5 6 | 7 | dependencies { 8 | compile files(android_jar) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/codebutler/android_websockets/HybiParser.java: -------------------------------------------------------------------------------- 1 | // 2 | // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser 3 | // 4 | // Based on code from the faye project. 5 | // https://github.com/faye/faye-websocket-node 6 | // Copyright (c) 2009-2012 James Coglan 7 | // 8 | // Ported from Javascript to Java by Eric Butler 9 | // 10 | // (The MIT License) 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining 13 | // a copy of this software and associated documentation files (the 14 | // "Software"), to deal in the Software without restriction, including 15 | // without limitation the rights to use, copy, modify, merge, publish, 16 | // distribute, sublicense, and/or sell copies of the Software, and to 17 | // permit persons to whom the Software is furnished to do so, subject to 18 | // the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be 21 | // included in all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | package com.codebutler.android_websockets; 32 | 33 | import android.util.Log; 34 | 35 | import java.io.*; 36 | import java.util.Arrays; 37 | import java.util.List; 38 | 39 | public class HybiParser { 40 | private static final String TAG = "HybiParser"; 41 | 42 | private WebSocketClient mClient; 43 | 44 | private boolean mMasking = true; 45 | 46 | private int mStage; 47 | 48 | private boolean mFinal; 49 | private boolean mMasked; 50 | private int mOpcode; 51 | private int mLengthSize; 52 | private int mLength; 53 | private int mMode; 54 | 55 | private byte[] mMask = new byte[0]; 56 | private byte[] mPayload = new byte[0]; 57 | 58 | private boolean mClosed = false; 59 | 60 | private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); 61 | 62 | private static final int BYTE = 255; 63 | private static final int FIN = 128; 64 | private static final int MASK = 128; 65 | private static final int RSV1 = 64; 66 | private static final int RSV2 = 32; 67 | private static final int RSV3 = 16; 68 | private static final int OPCODE = 15; 69 | private static final int LENGTH = 127; 70 | 71 | private static final int MODE_TEXT = 1; 72 | private static final int MODE_BINARY = 2; 73 | 74 | private static final int OP_CONTINUATION = 0; 75 | private static final int OP_TEXT = 1; 76 | private static final int OP_BINARY = 2; 77 | private static final int OP_CLOSE = 8; 78 | private static final int OP_PING = 9; 79 | private static final int OP_PONG = 10; 80 | 81 | private static final List OPCODES = Arrays.asList( 82 | OP_CONTINUATION, 83 | OP_TEXT, 84 | OP_BINARY, 85 | OP_CLOSE, 86 | OP_PING, 87 | OP_PONG 88 | ); 89 | 90 | private static final List FRAGMENTED_OPCODES = Arrays.asList( 91 | OP_CONTINUATION, OP_TEXT, OP_BINARY 92 | ); 93 | 94 | public HybiParser(WebSocketClient client) { 95 | mClient = client; 96 | } 97 | 98 | private static byte[] mask(byte[] payload, byte[] mask, int offset) { 99 | if (mask.length == 0) return payload; 100 | 101 | for (int i = 0; i < payload.length - offset; i++) { 102 | payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); 103 | } 104 | return payload; 105 | } 106 | 107 | public void start(HappyDataInputStream stream) throws IOException { 108 | while (true) { 109 | if (stream.available() == -1) break; 110 | switch (mStage) { 111 | case 0: 112 | parseOpcode(stream.readByte()); 113 | break; 114 | case 1: 115 | parseLength(stream.readByte()); 116 | break; 117 | case 2: 118 | parseExtendedLength(stream.readBytes(mLengthSize)); 119 | break; 120 | case 3: 121 | mMask = stream.readBytes(4); 122 | mStage = 4; 123 | break; 124 | case 4: 125 | mPayload = stream.readBytes(mLength); 126 | emitFrame(); 127 | mStage = 0; 128 | break; 129 | } 130 | } 131 | mClient.getListener().onDisconnect(0, "EOF"); 132 | } 133 | 134 | private void parseOpcode(byte data) throws ProtocolError { 135 | boolean rsv1 = (data & RSV1) == RSV1; 136 | boolean rsv2 = (data & RSV2) == RSV2; 137 | boolean rsv3 = (data & RSV3) == RSV3; 138 | 139 | if (rsv1 || rsv2 || rsv3) { 140 | throw new ProtocolError("RSV not zero"); 141 | } 142 | 143 | mFinal = (data & FIN) == FIN; 144 | mOpcode = (data & OPCODE); 145 | mMask = new byte[0]; 146 | mPayload = new byte[0]; 147 | 148 | if (!OPCODES.contains(mOpcode)) { 149 | throw new ProtocolError("Bad opcode"); 150 | } 151 | 152 | if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { 153 | throw new ProtocolError("Expected non-final packet"); 154 | } 155 | 156 | mStage = 1; 157 | } 158 | 159 | private void parseLength(byte data) { 160 | mMasked = (data & MASK) == MASK; 161 | mLength = (data & LENGTH); 162 | 163 | if (mLength >= 0 && mLength <= 125) { 164 | mStage = mMasked ? 3 : 4; 165 | } else { 166 | mLengthSize = (mLength == 126) ? 2 : 8; 167 | mStage = 2; 168 | } 169 | } 170 | 171 | private void parseExtendedLength(byte[] buffer) throws ProtocolError { 172 | mLength = getInteger(buffer); 173 | mStage = mMasked ? 3 : 4; 174 | } 175 | 176 | public byte[] frame(String data) { 177 | return frame(data, OP_TEXT, -1); 178 | } 179 | 180 | public byte[] frame(byte[] data) { 181 | return frame(data, OP_BINARY, -1); 182 | } 183 | 184 | private byte[] frame(byte[] data, int opcode, int errorCode) { 185 | return frame((Object)data, opcode, errorCode); 186 | } 187 | 188 | private byte[] frame(String data, int opcode, int errorCode) { 189 | return frame((Object)data, opcode, errorCode); 190 | } 191 | 192 | private byte[] frame(Object data, int opcode, int errorCode) { 193 | if (mClosed) return null; 194 | 195 | Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); 196 | 197 | byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data; 198 | int insert = (errorCode > 0) ? 2 : 0; 199 | int length = buffer.length + insert; 200 | int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); 201 | int offset = header + (mMasking ? 4 : 0); 202 | int masked = mMasking ? MASK : 0; 203 | byte[] frame = new byte[length + offset]; 204 | 205 | frame[0] = (byte) ((byte)FIN | (byte)opcode); 206 | 207 | if (length <= 125) { 208 | frame[1] = (byte) (masked | length); 209 | } else if (length <= 65535) { 210 | frame[1] = (byte) (masked | 126); 211 | frame[2] = (byte) Math.floor(length / 256); 212 | frame[3] = (byte) (length & BYTE); 213 | } else { 214 | frame[1] = (byte) (masked | 127); 215 | frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE); 216 | frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE); 217 | frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE); 218 | frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE); 219 | frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE); 220 | frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE); 221 | frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE); 222 | frame[9] = (byte) (length & BYTE); 223 | } 224 | 225 | if (errorCode > 0) { 226 | frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE); 227 | frame[offset+1] = (byte) (errorCode & BYTE); 228 | } 229 | System.arraycopy(buffer, 0, frame, offset + insert, buffer.length); 230 | 231 | if (mMasking) { 232 | byte[] mask = { 233 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), 234 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) 235 | }; 236 | System.arraycopy(mask, 0, frame, header, mask.length); 237 | mask(frame, mask, offset); 238 | } 239 | 240 | return frame; 241 | } 242 | 243 | public void ping(String message) { 244 | mClient.send(frame(message, OP_PING, -1)); 245 | } 246 | 247 | public void close(int code, String reason) { 248 | if (mClosed) return; 249 | mClient.send(frame(reason, OP_CLOSE, code)); 250 | mClosed = true; 251 | } 252 | 253 | private void emitFrame() throws IOException { 254 | byte[] payload = mask(mPayload, mMask, 0); 255 | int opcode = mOpcode; 256 | 257 | if (opcode == OP_CONTINUATION) { 258 | if (mMode == 0) { 259 | throw new ProtocolError("Mode was not set."); 260 | } 261 | mBuffer.write(payload); 262 | if (mFinal) { 263 | byte[] message = mBuffer.toByteArray(); 264 | if (mMode == MODE_TEXT) { 265 | mClient.getListener().onMessage(encode(message)); 266 | } else { 267 | mClient.getListener().onMessage(message); 268 | } 269 | reset(); 270 | } 271 | 272 | } else if (opcode == OP_TEXT) { 273 | if (mFinal) { 274 | String messageText = encode(payload); 275 | mClient.getListener().onMessage(messageText); 276 | } else { 277 | mMode = MODE_TEXT; 278 | mBuffer.write(payload); 279 | } 280 | 281 | } else if (opcode == OP_BINARY) { 282 | if (mFinal) { 283 | mClient.getListener().onMessage(payload); 284 | } else { 285 | mMode = MODE_BINARY; 286 | mBuffer.write(payload); 287 | } 288 | 289 | } else if (opcode == OP_CLOSE) { 290 | int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0; 291 | String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; 292 | Log.d(TAG, "Got close op! " + code + " " + reason); 293 | mClient.getListener().onDisconnect(code, reason); 294 | 295 | } else if (opcode == OP_PING) { 296 | if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } 297 | Log.d(TAG, "Sending pong!!"); 298 | mClient.sendFrame(frame(payload, OP_PONG, -1)); 299 | 300 | } else if (opcode == OP_PONG) { 301 | String message = encode(payload); 302 | // FIXME: Fire callback... 303 | Log.d(TAG, "Got pong! " + message); 304 | } 305 | } 306 | 307 | private void reset() { 308 | mMode = 0; 309 | mBuffer.reset(); 310 | } 311 | 312 | private String encode(byte[] buffer) { 313 | try { 314 | return new String(buffer, "UTF-8"); 315 | } catch (UnsupportedEncodingException e) { 316 | throw new RuntimeException(e); 317 | } 318 | } 319 | 320 | private byte[] decode(String string) { 321 | try { 322 | return (string).getBytes("UTF-8"); 323 | } catch (UnsupportedEncodingException e) { 324 | throw new RuntimeException(e); 325 | } 326 | } 327 | 328 | private int getInteger(byte[] bytes) throws ProtocolError { 329 | long i = byteArrayToLong(bytes, 0, bytes.length); 330 | if (i < 0 || i > Integer.MAX_VALUE) { 331 | throw new ProtocolError("Bad integer: " + i); 332 | } 333 | return (int) i; 334 | } 335 | 336 | private byte[] slice(byte[] array, int start) { 337 | return Arrays.copyOfRange(array, start, array.length); 338 | } 339 | 340 | public static class ProtocolError extends IOException { 341 | public ProtocolError(String detailMessage) { 342 | super(detailMessage); 343 | } 344 | } 345 | 346 | private static long byteArrayToLong(byte[] b, int offset, int length) { 347 | if (b.length < length) 348 | throw new IllegalArgumentException("length must be less than or equal to b.length"); 349 | 350 | long value = 0; 351 | for (int i = 0; i < length; i++) { 352 | int shift = (length - 1 - i) * 8; 353 | value += (b[i + offset] & 0x000000FF) << shift; 354 | } 355 | return value; 356 | } 357 | 358 | public static class HappyDataInputStream extends DataInputStream { 359 | public HappyDataInputStream(InputStream in) { 360 | super(in); 361 | } 362 | 363 | public byte[] readBytes(int length) throws IOException { 364 | byte[] buffer = new byte[length]; 365 | readFully(buffer); 366 | return buffer; 367 | } 368 | } 369 | } -------------------------------------------------------------------------------- /src/main/java/com/codebutler/android_websockets/WebSocketClient.java: -------------------------------------------------------------------------------- 1 | package com.codebutler.android_websockets; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.text.TextUtils; 6 | import android.util.Base64; 7 | import android.util.Log; 8 | import org.apache.http.*; 9 | import org.apache.http.client.HttpResponseException; 10 | import org.apache.http.message.BasicLineParser; 11 | import org.apache.http.message.BasicNameValuePair; 12 | 13 | import javax.net.SocketFactory; 14 | import javax.net.ssl.SSLContext; 15 | import javax.net.ssl.SSLException; 16 | import javax.net.ssl.SSLSocketFactory; 17 | import javax.net.ssl.TrustManager; 18 | import java.io.EOFException; 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.io.PrintWriter; 22 | import java.net.Socket; 23 | import java.net.URI; 24 | import java.security.KeyManagementException; 25 | import java.security.MessageDigest; 26 | import java.security.NoSuchAlgorithmException; 27 | import java.util.List; 28 | 29 | public class WebSocketClient { 30 | private static final String TAG = "WebSocketClient"; 31 | 32 | private URI mURI; 33 | private Listener mListener; 34 | private Socket mSocket; 35 | private Thread mThread; 36 | private HandlerThread mHandlerThread; 37 | private Handler mHandler; 38 | private List mExtraHeaders; 39 | private HybiParser mParser; 40 | 41 | private final Object mSendLock = new Object(); 42 | 43 | private static TrustManager[] sTrustManagers; 44 | 45 | public static void setTrustManagers(TrustManager[] tm) { 46 | sTrustManagers = tm; 47 | } 48 | 49 | public WebSocketClient(URI uri, Listener listener, List extraHeaders) { 50 | mURI = uri; 51 | mListener = listener; 52 | mExtraHeaders = extraHeaders; 53 | mParser = new HybiParser(this); 54 | 55 | mHandlerThread = new HandlerThread("websocket-thread"); 56 | mHandlerThread.start(); 57 | mHandler = new Handler(mHandlerThread.getLooper()); 58 | } 59 | 60 | public Listener getListener() { 61 | return mListener; 62 | } 63 | 64 | public void connect() { 65 | if (mThread != null && mThread.isAlive()) { 66 | return; 67 | } 68 | 69 | mThread = new Thread(new Runnable() { 70 | @Override 71 | public void run() { 72 | try { 73 | String secret = createSecret(); 74 | 75 | int port = (mURI.getPort() != -1) ? mURI.getPort() : (mURI.getScheme().equals("wss") ? 443 : 80); 76 | 77 | String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath(); 78 | if (!TextUtils.isEmpty(mURI.getQuery())) { 79 | path += "?" + mURI.getQuery(); 80 | } 81 | 82 | String originScheme = mURI.getScheme().equals("wss") ? "https" : "http"; 83 | URI origin = new URI(originScheme, "//" + mURI.getHost(), null); 84 | 85 | SocketFactory factory = mURI.getScheme().equals("wss") ? getSSLSocketFactory() : SocketFactory.getDefault(); 86 | mSocket = factory.createSocket(mURI.getHost(), port); 87 | 88 | PrintWriter out = new PrintWriter(mSocket.getOutputStream()); 89 | out.print("GET " + path + " HTTP/1.1\r\n"); 90 | out.print("Upgrade: websocket\r\n"); 91 | out.print("Connection: Upgrade\r\n"); 92 | out.print("Host: " + mURI.getHost() + "\r\n"); 93 | out.print("Origin: " + origin.toString() + "\r\n"); 94 | out.print("Sec-WebSocket-Key: " + secret + "\r\n"); 95 | out.print("Sec-WebSocket-Version: 13\r\n"); 96 | if (mExtraHeaders != null) { 97 | for (NameValuePair pair : mExtraHeaders) { 98 | out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue())); 99 | } 100 | } 101 | out.print("\r\n"); 102 | out.flush(); 103 | 104 | HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); 105 | 106 | // Read HTTP response status line. 107 | StatusLine statusLine = parseStatusLine(readLine(stream)); 108 | if (statusLine == null) { 109 | throw new HttpException("Received no reply from server."); 110 | } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) { 111 | throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); 112 | } 113 | 114 | // Read HTTP response headers. 115 | String line; 116 | boolean validated = false; 117 | 118 | while (!TextUtils.isEmpty(line = readLine(stream))) { 119 | Header header = parseHeader(line); 120 | if (header.getName().equals("Sec-WebSocket-Accept")) { 121 | String expected = createSecretValidation(secret); 122 | String actual = header.getValue().trim(); 123 | 124 | if (!expected.equals(actual)) { 125 | throw new HttpException("Bad Sec-WebSocket-Accept header value."); 126 | } 127 | 128 | validated = true; 129 | } 130 | } 131 | 132 | if (!validated) { 133 | throw new HttpException("No Sec-WebSocket-Accept header."); 134 | } 135 | 136 | mListener.onConnect(); 137 | 138 | // Now decode websocket frames. 139 | mParser.start(stream); 140 | 141 | } catch (EOFException ex) { 142 | Log.d(TAG, "WebSocket EOF!", ex); 143 | mListener.onDisconnect(0, "EOF"); 144 | 145 | } catch (SSLException ex) { 146 | // Connection reset by peer 147 | Log.d(TAG, "Websocket SSL error!", ex); 148 | mListener.onDisconnect(0, "SSL"); 149 | 150 | } catch (Exception ex) { 151 | mListener.onError(ex); 152 | } 153 | } 154 | }); 155 | mThread.start(); 156 | } 157 | 158 | public void disconnect() { 159 | if (mSocket != null) { 160 | mHandler.post(new Runnable() { 161 | @Override 162 | public void run() { 163 | try { 164 | mSocket.close(); 165 | mSocket = null; 166 | } catch (IOException ex) { 167 | Log.d(TAG, "Error while disconnecting", ex); 168 | mListener.onError(ex); 169 | } 170 | } 171 | }); 172 | } 173 | } 174 | 175 | public void send(String data) { 176 | sendFrame(mParser.frame(data)); 177 | } 178 | 179 | public void send(byte[] data) { 180 | sendFrame(mParser.frame(data)); 181 | } 182 | 183 | private StatusLine parseStatusLine(String line) { 184 | if (TextUtils.isEmpty(line)) { 185 | return null; 186 | } 187 | return BasicLineParser.parseStatusLine(line, new BasicLineParser()); 188 | } 189 | 190 | private Header parseHeader(String line) { 191 | return BasicLineParser.parseHeader(line, new BasicLineParser()); 192 | } 193 | 194 | // Can't use BufferedReader because it buffers past the HTTP data. 195 | private String readLine(HybiParser.HappyDataInputStream reader) throws IOException { 196 | int readChar = reader.read(); 197 | if (readChar == -1) { 198 | return null; 199 | } 200 | StringBuilder string = new StringBuilder(""); 201 | while (readChar != '\n') { 202 | if (readChar != '\r') { 203 | string.append((char) readChar); 204 | } 205 | 206 | readChar = reader.read(); 207 | if (readChar == -1) { 208 | return null; 209 | } 210 | } 211 | return string.toString(); 212 | } 213 | 214 | private String createSecret() { 215 | byte[] nonce = new byte[16]; 216 | for (int i = 0; i < 16; i++) { 217 | nonce[i] = (byte) (Math.random() * 256); 218 | } 219 | return Base64.encodeToString(nonce, Base64.DEFAULT).trim(); 220 | } 221 | 222 | private String createSecretValidation(String secret) { 223 | try { 224 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 225 | md.update((secret + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes()); 226 | return Base64.encodeToString(md.digest(), Base64.DEFAULT).trim(); 227 | } catch (NoSuchAlgorithmException e) { 228 | throw new RuntimeException(e); 229 | } 230 | } 231 | 232 | void sendFrame(final byte[] frame) { 233 | mHandler.post(new Runnable() { 234 | @Override 235 | public void run() { 236 | try { 237 | synchronized (mSendLock) { 238 | if (mSocket == null) { 239 | throw new IllegalStateException("Socket not connected"); 240 | } 241 | OutputStream outputStream = mSocket.getOutputStream(); 242 | outputStream.write(frame); 243 | outputStream.flush(); 244 | } 245 | } catch (IOException e) { 246 | mListener.onError(e); 247 | } 248 | } 249 | }); 250 | } 251 | 252 | public interface Listener { 253 | public void onConnect(); 254 | public void onMessage(String message); 255 | public void onMessage(byte[] data); 256 | public void onDisconnect(int code, String reason); 257 | public void onError(Exception error); 258 | } 259 | 260 | private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { 261 | SSLContext context = SSLContext.getInstance("TLS"); 262 | context.init(null, sTrustManagers, null); 263 | return context.getSocketFactory(); 264 | } 265 | } 266 | --------------------------------------------------------------------------------