michael@0: // michael@0: // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser michael@0: // michael@0: // Based on code from the faye project. michael@0: // https://github.com/faye/faye-websocket-node michael@0: // Copyright (c) 2009-2012 James Coglan michael@0: // michael@0: // Ported from Javascript to Java by Eric Butler michael@0: // michael@0: // (The MIT License) michael@0: // michael@0: // Permission is hereby granted, free of charge, to any person obtaining michael@0: // a copy of this software and associated documentation files (the michael@0: // "Software"), to deal in the Software without restriction, including michael@0: // without limitation the rights to use, copy, modify, merge, publish, michael@0: // distribute, sublicense, and/or sell copies of the Software, and to michael@0: // permit persons to whom the Software is furnished to do so, subject to michael@0: // the following conditions: michael@0: // michael@0: // The above copyright notice and this permission notice shall be michael@0: // included in all copies or substantial portions of the Software. michael@0: // michael@0: // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, michael@0: // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF michael@0: // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND michael@0: // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE michael@0: // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION michael@0: // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION michael@0: // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. michael@0: michael@0: package com.codebutler.android_websockets; michael@0: michael@0: import android.util.Log; michael@0: michael@0: import java.io.*; michael@0: import java.util.Arrays; michael@0: import java.util.List; michael@0: michael@0: public class HybiParser { michael@0: private static final String TAG = "HybiParser"; michael@0: michael@0: private WebSocketClient mClient; michael@0: michael@0: private boolean mMasking = true; michael@0: michael@0: private int mStage; michael@0: michael@0: private boolean mFinal; michael@0: private boolean mMasked; michael@0: private int mOpcode; michael@0: private int mLengthSize; michael@0: private int mLength; michael@0: private int mMode; michael@0: michael@0: private byte[] mMask = new byte[0]; michael@0: private byte[] mPayload = new byte[0]; michael@0: michael@0: private boolean mClosed = false; michael@0: michael@0: private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); michael@0: michael@0: private static final int BYTE = 255; michael@0: private static final int FIN = 128; michael@0: private static final int MASK = 128; michael@0: private static final int RSV1 = 64; michael@0: private static final int RSV2 = 32; michael@0: private static final int RSV3 = 16; michael@0: private static final int OPCODE = 15; michael@0: private static final int LENGTH = 127; michael@0: michael@0: private static final int MODE_TEXT = 1; michael@0: private static final int MODE_BINARY = 2; michael@0: michael@0: private static final int OP_CONTINUATION = 0; michael@0: private static final int OP_TEXT = 1; michael@0: private static final int OP_BINARY = 2; michael@0: private static final int OP_CLOSE = 8; michael@0: private static final int OP_PING = 9; michael@0: private static final int OP_PONG = 10; michael@0: michael@0: private static final List OPCODES = Arrays.asList( michael@0: OP_CONTINUATION, michael@0: OP_TEXT, michael@0: OP_BINARY, michael@0: OP_CLOSE, michael@0: OP_PING, michael@0: OP_PONG michael@0: ); michael@0: michael@0: private static final List FRAGMENTED_OPCODES = Arrays.asList( michael@0: OP_CONTINUATION, OP_TEXT, OP_BINARY michael@0: ); michael@0: michael@0: public HybiParser(WebSocketClient client) { michael@0: mClient = client; michael@0: } michael@0: michael@0: private static byte[] mask(byte[] payload, byte[] mask, int offset) { michael@0: if (mask.length == 0) return payload; michael@0: michael@0: for (int i = 0; i < payload.length - offset; i++) { michael@0: payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); michael@0: } michael@0: return payload; michael@0: } michael@0: michael@0: public void start(HappyDataInputStream stream) throws IOException { michael@0: while (true) { michael@0: if (stream.available() == -1) break; michael@0: switch (mStage) { michael@0: case 0: michael@0: parseOpcode(stream.readByte()); michael@0: break; michael@0: case 1: michael@0: parseLength(stream.readByte()); michael@0: break; michael@0: case 2: michael@0: parseExtendedLength(stream.readBytes(mLengthSize)); michael@0: break; michael@0: case 3: michael@0: mMask = stream.readBytes(4); michael@0: mStage = 4; michael@0: break; michael@0: case 4: michael@0: mPayload = stream.readBytes(mLength); michael@0: emitFrame(); michael@0: mStage = 0; michael@0: break; michael@0: } michael@0: } michael@0: mClient.getListener().onDisconnect(0, "EOF"); michael@0: } michael@0: michael@0: private void parseOpcode(byte data) throws ProtocolError { michael@0: boolean rsv1 = (data & RSV1) == RSV1; michael@0: boolean rsv2 = (data & RSV2) == RSV2; michael@0: boolean rsv3 = (data & RSV3) == RSV3; michael@0: michael@0: if (rsv1 || rsv2 || rsv3) { michael@0: throw new ProtocolError("RSV not zero"); michael@0: } michael@0: michael@0: mFinal = (data & FIN) == FIN; michael@0: mOpcode = (data & OPCODE); michael@0: mMask = new byte[0]; michael@0: mPayload = new byte[0]; michael@0: michael@0: if (!OPCODES.contains(mOpcode)) { michael@0: throw new ProtocolError("Bad opcode"); michael@0: } michael@0: michael@0: if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { michael@0: throw new ProtocolError("Expected non-final packet"); michael@0: } michael@0: michael@0: mStage = 1; michael@0: } michael@0: michael@0: private void parseLength(byte data) { michael@0: mMasked = (data & MASK) == MASK; michael@0: mLength = (data & LENGTH); michael@0: michael@0: if (mLength >= 0 && mLength <= 125) { michael@0: mStage = mMasked ? 3 : 4; michael@0: } else { michael@0: mLengthSize = (mLength == 126) ? 2 : 8; michael@0: mStage = 2; michael@0: } michael@0: } michael@0: michael@0: private void parseExtendedLength(byte[] buffer) throws ProtocolError { michael@0: mLength = getInteger(buffer); michael@0: mStage = mMasked ? 3 : 4; michael@0: } michael@0: michael@0: public byte[] frame(String data) { michael@0: return frame(data, OP_TEXT, -1); michael@0: } michael@0: michael@0: public byte[] frame(byte[] data) { michael@0: return frame(data, OP_BINARY, -1); michael@0: } michael@0: michael@0: private byte[] frame(byte[] data, int opcode, int errorCode) { michael@0: return frame((Object)data, opcode, errorCode); michael@0: } michael@0: michael@0: private byte[] frame(String data, int opcode, int errorCode) { michael@0: return frame((Object)data, opcode, errorCode); michael@0: } michael@0: michael@0: private byte[] frame(Object data, int opcode, int errorCode) { michael@0: if (mClosed) return null; michael@0: michael@0: Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); michael@0: michael@0: byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data; michael@0: int insert = (errorCode > 0) ? 2 : 0; michael@0: int length = buffer.length + insert; michael@0: int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); michael@0: int offset = header + (mMasking ? 4 : 0); michael@0: int masked = mMasking ? MASK : 0; michael@0: byte[] frame = new byte[length + offset]; michael@0: michael@0: frame[0] = (byte) ((byte)FIN | (byte)opcode); michael@0: michael@0: if (length <= 125) { michael@0: frame[1] = (byte) (masked | length); michael@0: } else if (length <= 65535) { michael@0: frame[1] = (byte) (masked | 126); michael@0: frame[2] = (byte) Math.floor(length / 256); michael@0: frame[3] = (byte) (length & BYTE); michael@0: } else { michael@0: frame[1] = (byte) (masked | 127); michael@0: frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE); michael@0: frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE); michael@0: frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE); michael@0: frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE); michael@0: frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE); michael@0: frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE); michael@0: frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE); michael@0: frame[9] = (byte) (length & BYTE); michael@0: } michael@0: michael@0: if (errorCode > 0) { michael@0: frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE); michael@0: frame[offset+1] = (byte) (errorCode & BYTE); michael@0: } michael@0: System.arraycopy(buffer, 0, frame, offset + insert, buffer.length); michael@0: michael@0: if (mMasking) { michael@0: byte[] mask = { michael@0: (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), michael@0: (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) michael@0: }; michael@0: System.arraycopy(mask, 0, frame, header, mask.length); michael@0: mask(frame, mask, offset); michael@0: } michael@0: michael@0: return frame; michael@0: } michael@0: michael@0: public void ping(String message) { michael@0: mClient.send(frame(message, OP_PING, -1)); michael@0: } michael@0: michael@0: public void close(int code, String reason) { michael@0: if (mClosed) return; michael@0: mClient.send(frame(reason, OP_CLOSE, code)); michael@0: mClosed = true; michael@0: } michael@0: michael@0: private void emitFrame() throws IOException { michael@0: byte[] payload = mask(mPayload, mMask, 0); michael@0: int opcode = mOpcode; michael@0: michael@0: if (opcode == OP_CONTINUATION) { michael@0: if (mMode == 0) { michael@0: throw new ProtocolError("Mode was not set."); michael@0: } michael@0: mBuffer.write(payload); michael@0: if (mFinal) { michael@0: byte[] message = mBuffer.toByteArray(); michael@0: if (mMode == MODE_TEXT) { michael@0: mClient.getListener().onMessage(encode(message)); michael@0: } else { michael@0: mClient.getListener().onMessage(message); michael@0: } michael@0: reset(); michael@0: } michael@0: michael@0: } else if (opcode == OP_TEXT) { michael@0: if (mFinal) { michael@0: String messageText = encode(payload); michael@0: mClient.getListener().onMessage(messageText); michael@0: } else { michael@0: mMode = MODE_TEXT; michael@0: mBuffer.write(payload); michael@0: } michael@0: michael@0: } else if (opcode == OP_BINARY) { michael@0: if (mFinal) { michael@0: mClient.getListener().onMessage(payload); michael@0: } else { michael@0: mMode = MODE_BINARY; michael@0: mBuffer.write(payload); michael@0: } michael@0: michael@0: } else if (opcode == OP_CLOSE) { michael@0: int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0; michael@0: String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; michael@0: Log.d(TAG, "Got close op! " + code + " " + reason); michael@0: mClient.getListener().onDisconnect(code, reason); michael@0: michael@0: } else if (opcode == OP_PING) { michael@0: if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } michael@0: Log.d(TAG, "Sending pong!!"); michael@0: mClient.sendFrame(frame(payload, OP_PONG, -1)); michael@0: michael@0: } else if (opcode == OP_PONG) { michael@0: String message = encode(payload); michael@0: // FIXME: Fire callback... michael@0: Log.d(TAG, "Got pong! " + message); michael@0: } michael@0: } michael@0: michael@0: private void reset() { michael@0: mMode = 0; michael@0: mBuffer.reset(); michael@0: } michael@0: michael@0: private String encode(byte[] buffer) { michael@0: try { michael@0: return new String(buffer, "UTF-8"); michael@0: } catch (UnsupportedEncodingException e) { michael@0: throw new RuntimeException(e); michael@0: } michael@0: } michael@0: michael@0: private byte[] decode(String string) { michael@0: try { michael@0: return (string).getBytes("UTF-8"); michael@0: } catch (UnsupportedEncodingException e) { michael@0: throw new RuntimeException(e); michael@0: } michael@0: } michael@0: michael@0: private int getInteger(byte[] bytes) throws ProtocolError { michael@0: long i = byteArrayToLong(bytes, 0, bytes.length); michael@0: if (i < 0 || i > Integer.MAX_VALUE) { michael@0: throw new ProtocolError("Bad integer: " + i); michael@0: } michael@0: return (int) i; michael@0: } michael@0: michael@0: /** michael@0: * Copied from AOSP Arrays.java. michael@0: */ michael@0: /** michael@0: * Copies elements from {@code original} into a new array, from indexes start (inclusive) to michael@0: * end (exclusive). The original order of elements is preserved. michael@0: * If {@code end} is greater than {@code original.length}, the result is padded michael@0: * with the value {@code (byte) 0}. michael@0: * michael@0: * @param original the original array michael@0: * @param start the start index, inclusive michael@0: * @param end the end index, exclusive michael@0: * @return the new array michael@0: * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length} michael@0: * @throws IllegalArgumentException if {@code start > end} michael@0: * @throws NullPointerException if {@code original == null} michael@0: * @since 1.6 michael@0: */ michael@0: private static byte[] copyOfRange(byte[] original, int start, int end) { michael@0: if (start > end) { michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: int originalLength = original.length; michael@0: if (start < 0 || start > originalLength) { michael@0: throw new ArrayIndexOutOfBoundsException(); michael@0: } michael@0: int resultLength = end - start; michael@0: int copyLength = Math.min(resultLength, originalLength - start); michael@0: byte[] result = new byte[resultLength]; michael@0: System.arraycopy(original, start, result, 0, copyLength); michael@0: return result; michael@0: } michael@0: michael@0: private byte[] slice(byte[] array, int start) { michael@0: return copyOfRange(array, start, array.length); michael@0: } michael@0: michael@0: public static class ProtocolError extends IOException { michael@0: public ProtocolError(String detailMessage) { michael@0: super(detailMessage); michael@0: } michael@0: } michael@0: michael@0: private static long byteArrayToLong(byte[] b, int offset, int length) { michael@0: if (b.length < length) michael@0: throw new IllegalArgumentException("length must be less than or equal to b.length"); michael@0: michael@0: long value = 0; michael@0: for (int i = 0; i < length; i++) { michael@0: int shift = (length - 1 - i) * 8; michael@0: value += (b[i + offset] & 0x000000FF) << shift; michael@0: } michael@0: return value; michael@0: } michael@0: michael@0: public static class HappyDataInputStream extends DataInputStream { michael@0: public HappyDataInputStream(InputStream in) { michael@0: super(in); michael@0: } michael@0: michael@0: public byte[] readBytes(int length) throws IOException { michael@0: byte[] buffer = new byte[length]; michael@0: michael@0: int total = 0; michael@0: michael@0: while (total < length) { michael@0: int count = read(buffer, total, length - total); michael@0: if (count == -1) { michael@0: break; michael@0: } michael@0: total += count; michael@0: } michael@0: michael@0: if (total != length) { michael@0: throw new IOException(String.format("Read wrong number of bytes. Got: %s, Expected: %s.", total, length)); michael@0: } michael@0: michael@0: return buffer; michael@0: } michael@0: } michael@0: }