/** * Copyright 2007-2015, Kaazing Corporation. All rights reserved. * * 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 org.kaazing.netx.ws.internal.io; import static java.lang.Character.charCount; import static java.lang.Character.toChars; import static java.lang.String.format; import static org.kaazing.netx.ws.MessageType.EOS; import static org.kaazing.netx.ws.WsURLConnection.WS_PROTOCOL_ERROR; import static org.kaazing.netx.ws.internal.ext.flyweight.Flyweight.uint8Get; import static org.kaazing.netx.ws.internal.ext.flyweight.Opcode.BINARY; import static org.kaazing.netx.ws.internal.ext.flyweight.Opcode.CLOSE; import static org.kaazing.netx.ws.internal.ext.flyweight.Opcode.CONTINUATION; import static org.kaazing.netx.ws.internal.ext.flyweight.Opcode.TEXT; import static org.kaazing.netx.ws.internal.util.Utf8Util.initialDecodeUTF8; import static org.kaazing.netx.ws.internal.util.Utf8Util.remainingBytesUTF8; import static org.kaazing.netx.ws.internal.util.Utf8Util.remainingDecodeUTF8; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import org.kaazing.netx.ws.MessageReader; import org.kaazing.netx.ws.MessageType; import org.kaazing.netx.ws.internal.DefaultWebSocketContext; import org.kaazing.netx.ws.internal.WsURLConnectionImpl; import org.kaazing.netx.ws.internal.ext.WebSocketContext; import org.kaazing.netx.ws.internal.ext.flyweight.Flyweight; import org.kaazing.netx.ws.internal.ext.flyweight.Frame; import org.kaazing.netx.ws.internal.ext.flyweight.FrameRO; import org.kaazing.netx.ws.internal.ext.flyweight.FrameRW; import org.kaazing.netx.ws.internal.ext.flyweight.Opcode; import org.kaazing.netx.ws.internal.ext.function.WebSocketFrameConsumer; import org.kaazing.netx.ws.internal.util.OptimisticReentrantLock; public class WsMessageReader extends MessageReader { private static final String MSG_NULL_CONNECTION = "Null connection passed in"; private static final String MSG_INDEX_OUT_OF_BOUNDS = "offset = %d; (offset + length) = %d; buffer length = %d"; private static final String MSG_NON_BINARY_FRAME = "Non-text frame - opcode = 0x%02X"; private static final String MSG_NON_TEXT_FRAME = "Non-binary frame - opcode = 0x%02X"; private static final String MSG_BUFFER_SIZE_SMALL = "Buffer's remaining capacity %d too small for payload of size %d"; private static final String MSG_MASKED_FRAME_FROM_SERVER = "Protocol Violation: Masked server-to-client frame"; private static final String MSG_RESERVED_BITS_SET = "Protocol Violation: Reserved bits set 0x%02X"; private static final String MSG_UNRECOGNIZED_OPCODE = "Protocol Violation: Unrecognized opcode %d"; private static final String MSG_FIRST_FRAME_FRAGMENTED = "Protocol Violation: First frame cannot be a fragmented frame"; private static final String MSG_UNEXPECTED_OPCODE = "Protocol Violation: Opcode 0x%02X expected only in the initial frame"; private static final String MSG_FRAGMENTED_CONTROL_FRAME = "Protocol Violation: Fragmented control frame 0x%02X"; private static final String MSG_FRAGMENTED_FRAME = "Protocol Violation: Fragmented frame 0x%02X"; private static final String MSG_NEXT_NOT_INVOKED = "MessageReader.next() method must be called before reading a message"; private static final String MSG_MAX_MESSAGE_LENGTH = "Message length %d is greater than the maximum allowed %d"; private static final String MSG_NOT_CURRENT_OWNER = "Thread reading the currrent message must perform this operation"; private static final String MSG_CANNOT_BE_READ_FULLY = "Message should be streamed as it spans across multiple frames"; private static final String MSG_CAN_BE_READ_FULLY = "Message can be read in it's entirety instead of streaming"; private static final String MSG_INVALID_MESSAGE_TYPE = "Invalid message type: '%s'"; private static final String MSG_BUFFER_OVERFLOW = "Buffer size '%d' small to accommodate a message of length '%d'"; private static final String MSG_END_OF_MESSAGE_STREAM = "End of message stream"; private enum State { INITIAL, PROCESS_FRAME; }; private final WsURLConnectionImpl connection; private final InputStream in; private final FrameRW incomingFrame; private final FrameRO incomingFrameRO; private final ByteBuffer heapBuffer; private final ByteBuffer heapBufferRO; private final byte[] networkBuffer; private final AtomicReference<Thread> currentMessageOwner; private final Lock lock; private int networkBufferReadOffset; private int networkBufferWriteOffset; private byte[] applicationByteBuffer; private char[] applicationCharBuffer; private int applicationBufferWriteOffset; private int applicationBufferLength; private int codePoint; private int remainingBytes; private MessageType type; private State state; private boolean fragmented; private int messageLength; // -1 for messages that span across multiple frames. private boolean finalFrame; private WsBinaryStream messageBinaryStream; private WsTextReader messageTextReader; final WebSocketFrameConsumer terminalBinaryFrameConsumer = new WebSocketFrameConsumer() { @Override public void accept(WebSocketContext context, Frame frame) throws IOException { Opcode opcode = frame.opcode(); long xformedPayloadLength = frame.payloadLength(); int xformedPayloadOffset = frame.payloadOffset(); switch (opcode) { case BINARY: case CONTINUATION: if ((opcode == BINARY) && fragmented) { byte leadByte = (byte) Flyweight.uint8Get(frame.buffer(), frame.offset()); connection.doFail(WS_PROTOCOL_ERROR, format(MSG_FRAGMENTED_FRAME, leadByte)); } if ((opcode == CONTINUATION) && !fragmented) { byte leadByte = (byte) Flyweight.uint8Get(frame.buffer(), frame.offset()); connection.doFail(WS_PROTOCOL_ERROR, format(MSG_FRAGMENTED_FRAME, leadByte)); } if (applicationBufferWriteOffset + xformedPayloadLength > applicationByteBuffer.length) { // MessageReader requires reading the entire message/frame. So, if there isn't enough space to read the // frame, we should throw an exception. int available = applicationByteBuffer.length - applicationBufferWriteOffset; throw new IOException(format(MSG_BUFFER_SIZE_SMALL, available, xformedPayloadLength)); } // Using System.arraycopy() to copy the contents of transformed.buffer().array() to the applicationBuffer // results in java.nio.ReadOnlyBufferException as we will be getting a RO flyweight in the terminal consumer. for (int i = 0; i < xformedPayloadLength; i++) { applicationByteBuffer[applicationBufferWriteOffset++] = frame.buffer().get(xformedPayloadOffset + i); } fragmented = !frame.fin(); break; default: connection.doFail(WS_PROTOCOL_ERROR, format(MSG_NON_BINARY_FRAME, Opcode.toInt(opcode))); break; } } }; private final WebSocketFrameConsumer terminalTextFrameConsumer = new WebSocketFrameConsumer() { @Override public void accept(WebSocketContext context, Frame frame) throws IOException { Opcode opcode = frame.opcode(); long xformedPayloadLength = frame.payloadLength(); int xformedPayloadOffset = frame.payloadOffset(); switch (opcode) { case TEXT: case CONTINUATION: if ((opcode == TEXT) && fragmented) { byte leadByte = (byte) Flyweight.uint8Get(frame.buffer(), frame.offset()); connection.doFail(WS_PROTOCOL_ERROR, format(MSG_FRAGMENTED_FRAME, leadByte)); } if ((opcode == CONTINUATION) && !fragmented) { byte leadByte = (byte) Flyweight.uint8Get(frame.buffer(), frame.offset()); connection.doFail(WS_PROTOCOL_ERROR, format(MSG_FRAGMENTED_FRAME, leadByte)); } int charsConverted = utf8BytesToChars(frame.buffer(), xformedPayloadOffset, xformedPayloadLength, applicationCharBuffer, applicationBufferWriteOffset, applicationBufferLength); applicationBufferWriteOffset += charsConverted; applicationBufferLength -= charsConverted; fragmented = !frame.fin(); break; default: connection.doFail(WS_PROTOCOL_ERROR, format(MSG_NON_BINARY_FRAME, Opcode.toInt(opcode))); break; } } }; private final WebSocketFrameConsumer terminalControlFrameConsumer = new WebSocketFrameConsumer() { @Override public void accept(WebSocketContext context, Frame frame) throws IOException { Opcode opcode = frame.opcode(); switch (opcode) { case CLOSE: connection.sendCloseIfNecessary(frame); break; case PING: connection.sendPong(frame); break; case PONG: break; default: connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNRECOGNIZED_OPCODE, Opcode.toInt(opcode))); break; } } }; public WsMessageReader(WsURLConnectionImpl connection) throws IOException { if (connection == null) { throw new NullPointerException(MSG_NULL_CONNECTION); } this.connection = connection; this.currentMessageOwner = new AtomicReference<Thread>(null); int maxFrameLength = connection.getMaxFrameLength(); this.in = connection.getTcpInputStream(); this.incomingFrame = new FrameRW(); this.incomingFrameRO = new FrameRO(); this.lock = new OptimisticReentrantLock(); this.state = State.INITIAL; this.fragmented = false; this.finalFrame = false; this.applicationBufferWriteOffset = 0; this.applicationBufferLength = 0; this.networkBufferReadOffset = 0; this.networkBufferWriteOffset = 0; this.networkBuffer = new byte[maxFrameLength]; this.heapBuffer = ByteBuffer.wrap(networkBuffer); this.heapBufferRO = heapBuffer.asReadOnlyBuffer(); } @Override public InputStream getInputStream() throws IOException { if (messageBinaryStream != null) { return messageBinaryStream; } try { lock.lock(); if (messageBinaryStream != null) { return messageBinaryStream; } messageBinaryStream = new WsBinaryStream(connection, this); } finally { lock.unlock(); } return messageBinaryStream; } @Override public Reader getReader() throws IOException { if (messageTextReader != null) { return messageTextReader; } try { lock.lock(); if (messageTextReader != null) { return messageTextReader; } messageTextReader = new WsTextReader(connection, this); } finally { lock.unlock(); } return messageTextReader; } @Override public MessageType next() throws IOException { while (!currentMessageOwner.compareAndSet(null, Thread.currentThread())) { // Spin till the current message has been completely read by the current owner. } switch (state) { case INITIAL: if (readDataFrameFully() == -1) { return EOS; } if (messageBinaryStream != null) { messageBinaryStream.resetState(); } if (messageTextReader != null) { messageTextReader.resetState(); } break; default: throw new IOException(MSG_NOT_CURRENT_OWNER); } return type; } @Override public MessageType peek() { return type; } @Override public int readFully(byte[] buffer) throws IOException { if (buffer == null) { throw new NullPointerException("Null buffer passed in"); } if (currentMessageOwner.get() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (currentMessageOwner.get() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } switch (type) { case EOS: return -1; case TEXT: throw new IOException(MSG_NON_BINARY_FRAME); default: break; } if (streaming()) { throw new IOException(MSG_CANNOT_BE_READ_FULLY); } if (messageLength > buffer.length) { throw new IOException(format(MSG_BUFFER_OVERFLOW, buffer.length, messageLength)); } assert finalFrame; int bytesRead = readAndProcessBinaryFrame(buffer, 0, buffer.length); messageLength = -1; resetCurrentOwner(); return bytesRead; } @Override public int readFully(char[] buffer) throws IOException { if (buffer == null) { throw new NullPointerException("Null buffer passed in"); } if (currentMessageOwner.get() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (currentMessageOwner.get() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } switch (type) { case EOS: return -1; case BINARY: throw new IOException(MSG_NON_TEXT_FRAME); default: break; } if (streaming()) { throw new IOException(MSG_CANNOT_BE_READ_FULLY); } assert finalFrame; int charsRead = readAndProcessTextFrame(buffer, 0, buffer.length); messageLength = -1; resetCurrentOwner(); return charsRead; } @Override public void skip() throws IOException { if (currentMessageOwner.get() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (currentMessageOwner.get() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (finalFrame) { // Skip a message that fits in a single frame. incomingFrame.wrap(heapBuffer, networkBufferReadOffset); networkBufferReadOffset += incomingFrame.length(); if (networkBufferReadOffset == networkBufferWriteOffset) { networkBufferReadOffset = 0; networkBufferWriteOffset = 0; } } else { // Skip a message spanning across multiple frames. while (!finalFrame) { readDataFrameFully(); } } state = State.INITIAL; currentMessageOwner.set(null); } @Override public boolean streaming() { if (currentMessageOwner.get() == null) { throw new IllegalStateException(MSG_NEXT_NOT_INVOKED); } if (currentMessageOwner.get() != Thread.currentThread()) { throw new IllegalStateException(MSG_NOT_CURRENT_OWNER); } return messageLength == -1; } public void close() throws IOException { try { lock.lock(); in.close(); type = null; state = null; } finally { lock.unlock(); } } // Package-Private Methods boolean isFinalFrame() { return finalFrame; } Thread getCurrentOwner() { return currentMessageOwner.get(); } void resetCurrentOwner() { currentMessageOwner.set(null); } // Private Methods private int readAndProcessBinaryFrame(byte[] buffer, int offset, int length) throws IOException { if (type != MessageType.BINARY) { throw new IOException(format(MSG_INVALID_MESSAGE_TYPE, type)); } applicationByteBuffer = buffer; applicationBufferWriteOffset = offset; if (readDataFrameFully() == -1) { return -1; } incomingFrame.wrap(heapBuffer, networkBufferReadOffset); finalFrame = incomingFrame.fin(); validateOpcode(); DefaultWebSocketContext context = connection.getIncomingContext(); IncomingSentinelExtension sentinel = (IncomingSentinelExtension) context.getSentinelExtension(); sentinel.setTerminalConsumer(terminalBinaryFrameConsumer, incomingFrame.opcode()); connection.processIncomingFrame(incomingFrameRO.wrap(heapBufferRO, networkBufferReadOffset)); networkBufferReadOffset += incomingFrame.length(); if (networkBufferReadOffset == networkBufferWriteOffset) { networkBufferReadOffset = 0; networkBufferWriteOffset = 0; } state = finalFrame ? State.INITIAL : State.PROCESS_FRAME; return applicationBufferWriteOffset - offset; } private int readAndProcessTextFrame(char[] buffer, int offset, int length) throws IOException { if (type != MessageType.TEXT) { throw new IOException(format(MSG_INVALID_MESSAGE_TYPE, type)); } applicationCharBuffer = buffer; applicationBufferWriteOffset = offset; applicationBufferLength = length; if (readDataFrameFully() == -1) { return -1; } incomingFrame.wrap(heapBuffer, networkBufferReadOffset); finalFrame = incomingFrame.fin(); validateOpcode(); DefaultWebSocketContext context = connection.getIncomingContext(); IncomingSentinelExtension sentinel = (IncomingSentinelExtension) context.getSentinelExtension(); sentinel.setTerminalConsumer(terminalTextFrameConsumer, incomingFrame.opcode()); connection.processIncomingFrame(incomingFrameRO.wrap(heapBufferRO, networkBufferReadOffset)); networkBufferReadOffset += incomingFrame.length(); if (networkBufferReadOffset == networkBufferWriteOffset) { networkBufferReadOffset = 0; networkBufferWriteOffset = 0; } state = finalFrame ? State.INITIAL : State.PROCESS_FRAME; return applicationBufferWriteOffset - offset; } // Returns the leadByte of the next data frame. Otherwise -1. private int readDataFrameFully() throws IOException { if (networkBufferWriteOffset == 0) { int bytesRead = in.read(networkBuffer, 0, networkBuffer.length); if (bytesRead == -1) { resetCurrentOwner(); type = EOS; return -1; } networkBufferReadOffset = 0; networkBufferWriteOffset = bytesRead; } int numBytes = ensureFrameMetadata(); if (numBytes == -1) { resetCurrentOwner(); type = EOS; return -1; } incomingFrame.wrap(heapBuffer, networkBufferReadOffset); int payloadLength = incomingFrame.payloadLength(); if (incomingFrame.offset() + payloadLength > networkBufferWriteOffset) { if (payloadLength > networkBuffer.length) { int maxPayloadLength = connection.getMaxFramePayloadLength(); throw new IOException(format(MSG_MAX_MESSAGE_LENGTH, payloadLength, maxPayloadLength)); } else { // Enough space. But may need shifting the frame to the beginning to be able to fit the payload. if (incomingFrame.offset() + payloadLength > networkBuffer.length) { int len = networkBufferWriteOffset - networkBufferReadOffset; System.arraycopy(networkBuffer, networkBufferReadOffset, networkBuffer, 0, len); networkBufferReadOffset = 0; networkBufferWriteOffset = len; } } int frameLength = connection.getFrameLength(false, payloadLength); int remainingBytes = networkBufferReadOffset + frameLength - networkBufferWriteOffset; while (remainingBytes > 0) { int bytesRead = in.read(networkBuffer, networkBufferWriteOffset, remainingBytes); if (bytesRead == -1) { resetCurrentOwner(); return -1; } remainingBytes -= bytesRead; networkBufferWriteOffset += bytesRead; } incomingFrame.wrap(heapBuffer, networkBufferReadOffset); } int leadByte = Flyweight.uint8Get(incomingFrame.buffer(), incomingFrame.offset()); int flags = incomingFrame.flags(); switch (flags) { case 0: break; default: connection.doFail(WS_PROTOCOL_ERROR, format(MSG_RESERVED_BITS_SET, flags)); break; } Opcode opcode = null; finalFrame = incomingFrame.fin(); try { opcode = incomingFrame.opcode(); } catch (Exception ex) { connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNRECOGNIZED_OPCODE, leadByte & 0x0F)); } byte maskByte = (byte) uint8Get(incomingFrame.buffer(), incomingFrame.offset() + 1); if ((maskByte & 0x80) != 0) { connection.doFail(WS_PROTOCOL_ERROR, MSG_MASKED_FRAME_FROM_SERVER); } switch (opcode) { case CONTINUATION: if (state == State.INITIAL) { // The first frame cannot be a fragmented frame.. type = MessageType.EOS; connection.doFail(WS_PROTOCOL_ERROR, MSG_FIRST_FRAME_FRAGMENTED); } break; case TEXT: if (state == State.PROCESS_FRAME) { // In a subsequent fragmented frame, the opcode should NOT be set. connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNEXPECTED_OPCODE, Opcode.toInt(TEXT))); } type = MessageType.TEXT; messageLength = (state == State.INITIAL) && finalFrame ? payloadLength : -1; break; case BINARY: if (state == State.PROCESS_FRAME) { // In a subsequent fragmented frame, the opcode should NOT be set. connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNEXPECTED_OPCODE, Opcode.toInt(BINARY))); } type = MessageType.BINARY; messageLength = (state == State.INITIAL) && finalFrame ? payloadLength : -1; break; case CLOSE: case PING: case PONG: if (!incomingFrame.fin()) { // Control frames cannot be fragmented. connection.doFail(WS_PROTOCOL_ERROR, MSG_FRAGMENTED_CONTROL_FRAME); } DefaultWebSocketContext context = connection.getIncomingContext(); IncomingSentinelExtension sentinel = (IncomingSentinelExtension) context.getSentinelExtension(); sentinel.setTerminalConsumer(terminalControlFrameConsumer, incomingFrame.opcode()); connection.processIncomingFrame(incomingFrameRO.wrap(heapBufferRO, networkBufferReadOffset)); networkBufferReadOffset += incomingFrame.length(); if (networkBufferReadOffset == networkBufferWriteOffset) { networkBufferReadOffset = 0; networkBufferWriteOffset = 0; } if (opcode == CLOSE) { type = MessageType.EOS; resetCurrentOwner(); return -1; } leadByte = readDataFrameFully(); break; default: connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNRECOGNIZED_OPCODE, opcode.ordinal())); break; } return leadByte; } private int utf8BytesToChars( ByteBuffer src, int srcOffset, long srcLength, char[] dest, int destOffset, int destLength) throws IOException { int destMark = destOffset; int index = 0; while (index < srcLength) { int b = -1; while (codePoint != 0 || ((index < srcLength) && (remainingBytes > 0))) { // Surrogate pair. if (codePoint != 0 && remainingBytes == 0) { int charCount = charCount(codePoint); if (charCount > destLength) { int len = destOffset + charCount; throw new IndexOutOfBoundsException(format(MSG_INDEX_OUT_OF_BOUNDS, destOffset, len, destLength)); } toChars(codePoint, dest, destOffset); destOffset += charCount; destLength -= charCount; codePoint = 0; break; } // EOP if (index == srcLength) { // We have multi-byte chars split across WebSocket frames. break; } b = src.get(srcOffset++); index++; // character encoded in multiple bytes codePoint = remainingDecodeUTF8(codePoint, remainingBytes--, b); } if (index < srcLength) { b = src.get(srcOffset++); index++; // Detect whether character is encoded using multiple bytes. remainingBytes = remainingBytesUTF8(b); switch (remainingBytes) { case 0: // No surrogate pair. int asciiCodePoint = initialDecodeUTF8(remainingBytes, b); assert charCount(asciiCodePoint) == 1; toChars(asciiCodePoint, dest, destOffset++); destLength--; break; default: codePoint = initialDecodeUTF8(remainingBytes, b); break; } } } return destOffset - destMark; } private int ensureFrameMetadata() throws IOException { int offsetDiff = networkBufferWriteOffset - networkBufferReadOffset; if (offsetDiff > 10) { // The payload length information is definitely available in the network buffer. return 0; } int bytesRead = 0; int maxMetadata = 10; int length = maxMetadata - offsetDiff; int frameMetadataLength = 2; // Ensure that the networkBuffer at the very least contains the payload length related bytes. switch (offsetDiff) { case 1: System.arraycopy(networkBuffer, networkBufferReadOffset, networkBuffer, 0, offsetDiff); networkBufferWriteOffset = offsetDiff; // no break case 0: length = frameMetadataLength - offsetDiff; while (length > 0) { bytesRead = in.read(networkBuffer, offsetDiff, length); if (bytesRead == -1) { return -1; } length -= bytesRead; networkBufferWriteOffset += bytesRead; } networkBufferReadOffset = 0; // no break; default: // int b1 = networkBuffer[networkBufferReadOffset]; // fin, flags and opcode int b2 = networkBuffer[networkBufferReadOffset + 1] & 0x7F; if (b2 > 0) { switch (b2) { case 126: frameMetadataLength += 2; break; case 127: frameMetadataLength += 8; break; default: break; } if (offsetDiff >= frameMetadataLength) { return 0; } int remainingMetadata = networkBufferReadOffset + frameMetadataLength - networkBufferWriteOffset; if (networkBuffer.length <= networkBufferWriteOffset + remainingMetadata) { // Shift the frame to the beginning of the buffer and try to read more bytes to be able to figure out // the payload length. System.arraycopy(networkBuffer, networkBufferReadOffset, networkBuffer, 0, offsetDiff); networkBufferReadOffset = 0; networkBufferWriteOffset = offsetDiff; } while (remainingMetadata > 0) { bytesRead = in.read(networkBuffer, networkBufferWriteOffset, remainingMetadata); if (bytesRead == -1) { return -1; } remainingMetadata -= bytesRead; networkBufferWriteOffset += bytesRead; } } } return bytesRead; } private void validateOpcode() throws IOException { int leadByte = Flyweight.uint8Get(incomingFrame.buffer(), incomingFrame.offset()); try { incomingFrame.opcode(); } catch (Exception ex) { connection.doFail(WS_PROTOCOL_ERROR, format(MSG_UNRECOGNIZED_OPCODE, leadByte & 0x0F)); } } private static class WsBinaryStream extends InputStream { private final byte[] binaryBuffer; private final WsMessageReader messageReader; private boolean fin; private int binaryBufferReadOffset; private int binaryBufferWriteOffset; public WsBinaryStream(WsURLConnectionImpl connection, WsMessageReader messageReader) { this.binaryBuffer = new byte[connection.getMaxFramePayloadLength()]; this.messageReader = messageReader; } @Override public int read() throws IOException { if (messageReader.getCurrentOwner() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (fin && (binaryBufferReadOffset == binaryBufferWriteOffset)) { return -1; } if (messageReader.getCurrentOwner() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (!messageReader.streaming()) { // This can be relaxed if we like. Meaning -- we can allow streaming of messages that fit in a single WebSocket // frame. throw new IOException(MSG_CAN_BE_READ_FULLY); } try { return readInternal(); } catch (RuntimeException ex) { return -1; } catch (IOException ex) { throw ex; } } @Override public int read(byte[] buf, int offset, int length) throws IOException { if (buf == null) { throw new NullPointerException("Null buffer passed in"); } else if ((offset < 0) || (length < 0) || (offset + length > buf.length)) { throw new IndexOutOfBoundsException(format(MSG_INDEX_OUT_OF_BOUNDS, offset, offset + length, buf.length)); } if (fin && (binaryBufferReadOffset == binaryBufferWriteOffset)) { return -1; } if (messageReader.getCurrentOwner() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (messageReader.getCurrentOwner() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (!messageReader.streaming()) { // This can be relaxed if we like. Meaning -- we can allow streaming of messages that fit in a single WebSocket // frame. throw new IOException(MSG_CAN_BE_READ_FULLY); } try { int mark = offset; try { populateBuffer(); int len = Math.min(length, binaryBufferWriteOffset - binaryBufferReadOffset); while (len > 0) { buf[offset] = (byte) readInternal(); len--; offset++; } } catch (RuntimeException ex) { int count = offset - mark; return count == 0 ? -1 : count; } return offset - mark; } catch (RuntimeException ex) { return -1; } catch (IOException ex) { throw ex; } } @Override public void close() throws IOException { if (messageReader.getCurrentOwner() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (messageReader.getCurrentOwner() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (!messageReader.streaming()) { throw new IOException(MSG_CAN_BE_READ_FULLY); } if (!fin) { messageReader.skip(); } resetState(); } void resetState() throws IOException { fin = false; binaryBufferReadOffset = 0; binaryBufferReadOffset = 0; } private void populateBuffer() throws IOException { while (binaryBufferReadOffset == binaryBufferWriteOffset) { binaryBufferWriteOffset = 0; binaryBufferReadOffset = 0; if (fin) { // The final frame of this message has been read. Make sure that read() returns -1. messageReader.resetCurrentOwner(); throw new RuntimeException(MSG_END_OF_MESSAGE_STREAM); } binaryBufferWriteOffset = messageReader.readAndProcessBinaryFrame(binaryBuffer, 0, binaryBuffer.length); fin = messageReader.isFinalFrame(); if (binaryBufferWriteOffset == -1) { binaryBufferWriteOffset = 0; throw new RuntimeException(MSG_END_OF_MESSAGE_STREAM); } else if (binaryBufferWriteOffset > 0) { break; } } } private int readInternal() throws IOException { populateBuffer(); assert binaryBufferReadOffset < binaryBufferWriteOffset; int b = binaryBuffer[binaryBufferReadOffset++]; if (binaryBufferReadOffset == binaryBufferWriteOffset) { binaryBufferReadOffset = 0; binaryBufferWriteOffset = 0; if (fin) { // The final frame of this message has been read. messageReader.resetCurrentOwner(); } } return b; } } private static class WsTextReader extends Reader { private final char[] textBuffer; private final WsMessageReader messageReader; private boolean fin; private int textBufferReadOffset; private int textBufferWriteOffset; public WsTextReader(WsURLConnectionImpl connection, WsMessageReader messageReader) { this.textBuffer = new char[connection.getMaxFramePayloadLength()]; this.messageReader = messageReader; } @Override public int read(char[] cbuf, int offset, int length) throws IOException { if ((offset < 0) || ((offset + length) > cbuf.length) || (length < 0)) { int len = offset + length; throw new IndexOutOfBoundsException(format(MSG_INDEX_OUT_OF_BOUNDS, offset, len, cbuf.length)); } if (fin && (textBufferReadOffset == textBufferWriteOffset)) { return -1; } if (messageReader.getCurrentOwner() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (messageReader.getCurrentOwner() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (!messageReader.streaming()) { throw new IOException(MSG_CAN_BE_READ_FULLY); } while (textBufferReadOffset == textBufferWriteOffset) { textBufferReadOffset = 0; textBufferWriteOffset = 0; if (fin) { // The final frame of this message has been read. messageReader.resetCurrentOwner(); return -1; } textBufferWriteOffset = messageReader.readAndProcessTextFrame(textBuffer, 0, textBuffer.length); if (textBufferWriteOffset == -1) { textBufferWriteOffset = 0; messageReader.resetCurrentOwner(); return -1; } fin = messageReader.isFinalFrame(); } assert textBufferReadOffset < textBufferWriteOffset; int charsRead = Math.min(length, textBufferWriteOffset - textBufferReadOffset); System.arraycopy(textBuffer, textBufferReadOffset, cbuf, offset, charsRead); textBufferReadOffset += charsRead; if (textBufferReadOffset == textBufferWriteOffset) { textBufferReadOffset = 0; textBufferWriteOffset = 0; if (fin) { // The final frame of this message has been read. messageReader.resetCurrentOwner(); } } return charsRead; } @Override public void close() throws IOException { if (messageReader.getCurrentOwner() == null) { throw new IOException(MSG_NEXT_NOT_INVOKED); } if (messageReader.getCurrentOwner() != Thread.currentThread()) { throw new IOException(MSG_NOT_CURRENT_OWNER); } if (!messageReader.streaming()) { throw new IOException(MSG_CAN_BE_READ_FULLY); } if (!fin) { messageReader.skip(); } resetState(); } void resetState() throws IOException { fin = false; textBufferReadOffset = 0; textBufferReadOffset = 0; } } }