/** * Generic function to send either a ping or a pong. * * @param data Optional message. * @param opcode The byte to include as the opcode. * @throws IOException If an error occurs writing to the client */ private void sendControlMessage(ByteBuffer data, byte opcode) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } doFlush(false); upgradeOutbound.write(0x80 | opcode); if (data == null) { upgradeOutbound.write(0); } else { upgradeOutbound.write(data.limit() - data.position()); upgradeOutbound.write(data.array(), data.position(), data.limit() - data.position()); } upgradeOutbound.flush(); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } }
/** * Adds the data to the buffer for binary data. If a textual message is currently in progress that * message will be completed and a new binary message started. If the buffer for binary data is * full, the buffer will be flushed and a new binary continuation fragment started. * * @param b The byte (only the least significant byte is used) of data to send to the client. * @throws IOException If a flush is required and an error occurs writing the WebSocket frame to * the client */ public void writeBinaryData(int b) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (bb.position() == bb.capacity()) { doFlush(false); } if (text == null) { text = Boolean.FALSE; } else if (text == Boolean.TRUE) { // Flush the character data flush(); text = Boolean.FALSE; } bb.put((byte) (b & 0xFF)); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } }
/** * Flush any message (binary or textual) that may be buffered and then send a WebSocket text * message as a single frame with the provided buffer as the payload of the message. * * @param msgCb The buffer containing the payload * @throws IOException If an error occurs writing to the client */ public void writeTextMessage(CharBuffer msgCb) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (text != null) { // Empty the buffer flush(); } text = Boolean.TRUE; doWriteText(msgCb, true); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } }
private boolean validateCloseStatus(int status) { if (status == Constants23.getStatusCloseNormal() || status == Constants23.getStatusShutdown() || status == Constants23.getStatusProtocolError() || status == Constants23.getStatusUnexpectedDataType() || status == Constants23.getStatusBadData() || status == Constants23.getStatusPolicyViolation() || status == Constants23.getStatusMessageTooLarge() || status == Constants23.getStatusRequiredExtension() || status == Constants23.getStatusUnexpectedCondition() || (status > 2999 && status < 5000)) { // Other 1xxx reserved / not permitted // 2xxx reserved // 3xxx framework defined // 4xxx application defined return true; } // <1000 unused // >4999 undefined return false; }
/** * Flush any message (binary or textual) that may be buffered. * * @throws IOException If an error occurs writing to the client */ public void flush() throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } doFlush(true); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } }
/** * Send a close message to the client * * @param status Must be a valid status code or zero to send no code * @param data Optional message. If message is defined, a valid status code must be provided. * @throws IOException If an error occurs writing to the client */ public void close(int status, ByteBuffer data) throws IOException { try { synchronized (stateLock) { if (closed) { return; } // Send any partial data we have try { doFlush(false); } finally { closed = true; } upgradeOutbound.write(0x88); if (status == 0) { upgradeOutbound.write(0); } else if (data == null || data.position() == data.limit()) { upgradeOutbound.write(2); upgradeOutbound.write(status >>> 8); upgradeOutbound.write(status); } else { upgradeOutbound.write(2 + data.limit() - data.position()); upgradeOutbound.write(status >>> 8); upgradeOutbound.write(status); upgradeOutbound.write(data.array(), data.position(), data.limit() - data.position()); } upgradeOutbound.flush(); bb = null; cb = null; upgradeOutbound = null; } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } }
/** * Respond to a client close by sending a close that echoes the status code and message. * * @param frame The close frame received from a client * @throws IOException If an error occurs writing to the client */ protected void close(WsFrame frame) throws IOException { if (frame.getPayLoadLength() > 0) { // Must be status (2 bytes) plus optional message if (frame.getPayLoadLength() == 1) { throw new IOException(); } int status = (frame.getPayLoad().get() & 0xFF) << 8; status += frame.getPayLoad().get() & 0xFF; if (validateCloseStatus(status)) { // Echo the status back to the client close(status, frame.getPayLoad()); } else { // Invalid close code close(Constants23.getStatusProtocolError(), null); } } else { // No status close(0, null); } }
/** * Send a ping message to the client * * @param data Optional message. * @throws IOException If an error occurs writing to the client */ public void ping(ByteBuffer data) throws IOException { sendControlMessage(data, Constants23.getOpcodePing()); }
/** * Provides the means to write WebSocket messages to the client. All methods that write to the * client (or update a buffer that is later written to the client) are synchronized to prevent * multiple threads trying to write to the client at the same time. * * @deprecated Replaced by the JSR356 WebSocket 1.1 implementation and will be removed in Tomcat * 8.0.x. */ @Deprecated public class WsOutbound { private static final StringManager3 sm = StringManager3.getManager(Constants23.getPackage()); private static final int DEFAULT_BUFFER_SIZE = 8192; /** * This state lock is used rather than synchronized methods to allow error handling to be managed * outside of the synchronization else deadlocks may occur such as * https://issues.apache.org/bugzilla/show_bug.cgi?id=55524 */ private final Object stateLock = new Object(); private UpgradeOutbound upgradeOutbound; private StreamInbound streamInbound; private ByteBuffer bb; private CharBuffer cb; private boolean closed = false; private Boolean text = null; private boolean firstFrame = true; public WsOutbound(UpgradeOutbound upgradeOutbound, StreamInbound streamInbound) { this(upgradeOutbound, streamInbound, DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); } public WsOutbound( UpgradeOutbound upgradeOutbound, StreamInbound streamInbound, int byteBufferSize, int charBufferSize) { this.upgradeOutbound = upgradeOutbound; this.streamInbound = streamInbound; this.bb = ByteBuffer.allocate(byteBufferSize); this.cb = CharBuffer.allocate(charBufferSize); } /** * Adds the data to the buffer for binary data. If a textual message is currently in progress that * message will be completed and a new binary message started. If the buffer for binary data is * full, the buffer will be flushed and a new binary continuation fragment started. * * @param b The byte (only the least significant byte is used) of data to send to the client. * @throws IOException If a flush is required and an error occurs writing the WebSocket frame to * the client */ public void writeBinaryData(int b) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (bb.position() == bb.capacity()) { doFlush(false); } if (text == null) { text = Boolean.FALSE; } else if (text == Boolean.TRUE) { // Flush the character data flush(); text = Boolean.FALSE; } bb.put((byte) (b & 0xFF)); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Adds the data to the buffer for textual data. If a binary message is currently in progress that * message will be completed and a new textual message started. If the buffer for textual data is * full, the buffer will be flushed and a new textual continuation fragment started. * * @param c The character to send to the client. * @throws IOException If a flush is required and an error occurs writing the WebSocket frame to * the client */ public void writeTextData(char c) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (cb.position() == cb.capacity()) { doFlush(false); } if (text == null) { text = Boolean.TRUE; } else if (text == Boolean.FALSE) { // Flush the binary data flush(); text = Boolean.TRUE; } cb.append(c); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Flush any message (binary or textual) that may be buffered and then send a WebSocket binary * message as a single frame with the provided buffer as the payload of the message. * * @param msgBb The buffer containing the payload * @throws IOException If an error occurs writing to the client */ public void writeBinaryMessage(ByteBuffer msgBb) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (text != null) { // Empty the buffer flush(); } text = Boolean.FALSE; doWriteBytes(msgBb, true); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Flush any message (binary or textual) that may be buffered and then send a WebSocket text * message as a single frame with the provided buffer as the payload of the message. * * @param msgCb The buffer containing the payload * @throws IOException If an error occurs writing to the client */ public void writeTextMessage(CharBuffer msgCb) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } if (text != null) { // Empty the buffer flush(); } text = Boolean.TRUE; doWriteText(msgCb, true); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Flush any message (binary or textual) that may be buffered. * * @throws IOException If an error occurs writing to the client */ public void flush() throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } doFlush(true); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } private void doFlush(boolean finalFragment) throws IOException { if (text == null) { // No data return; } if (text.booleanValue()) { cb.flip(); doWriteText(cb, finalFragment); } else { bb.flip(); doWriteBytes(bb, finalFragment); } } /** * Respond to a client close by sending a close that echoes the status code and message. * * @param frame The close frame received from a client * @throws IOException If an error occurs writing to the client */ protected void close(WsFrame frame) throws IOException { if (frame.getPayLoadLength() > 0) { // Must be status (2 bytes) plus optional message if (frame.getPayLoadLength() == 1) { throw new IOException(); } int status = (frame.getPayLoad().get() & 0xFF) << 8; status += frame.getPayLoad().get() & 0xFF; if (validateCloseStatus(status)) { // Echo the status back to the client close(status, frame.getPayLoad()); } else { // Invalid close code close(Constants23.getStatusProtocolError(), null); } } else { // No status close(0, null); } } private boolean validateCloseStatus(int status) { if (status == Constants23.getStatusCloseNormal() || status == Constants23.getStatusShutdown() || status == Constants23.getStatusProtocolError() || status == Constants23.getStatusUnexpectedDataType() || status == Constants23.getStatusBadData() || status == Constants23.getStatusPolicyViolation() || status == Constants23.getStatusMessageTooLarge() || status == Constants23.getStatusRequiredExtension() || status == Constants23.getStatusUnexpectedCondition() || (status > 2999 && status < 5000)) { // Other 1xxx reserved / not permitted // 2xxx reserved // 3xxx framework defined // 4xxx application defined return true; } // <1000 unused // >4999 undefined return false; } /** * Send a close message to the client * * @param status Must be a valid status code or zero to send no code * @param data Optional message. If message is defined, a valid status code must be provided. * @throws IOException If an error occurs writing to the client */ public void close(int status, ByteBuffer data) throws IOException { try { synchronized (stateLock) { if (closed) { return; } // Send any partial data we have try { doFlush(false); } finally { closed = true; } upgradeOutbound.write(0x88); if (status == 0) { upgradeOutbound.write(0); } else if (data == null || data.position() == data.limit()) { upgradeOutbound.write(2); upgradeOutbound.write(status >>> 8); upgradeOutbound.write(status); } else { upgradeOutbound.write(2 + data.limit() - data.position()); upgradeOutbound.write(status >>> 8); upgradeOutbound.write(status); upgradeOutbound.write(data.array(), data.position(), data.limit() - data.position()); } upgradeOutbound.flush(); bb = null; cb = null; upgradeOutbound = null; } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Send a pong message to the client * * @param data Optional message. * @throws IOException If an error occurs writing to the client */ public void pong(ByteBuffer data) throws IOException { sendControlMessage(data, Constants23.getOpcodePong()); } /** * Send a ping message to the client * * @param data Optional message. * @throws IOException If an error occurs writing to the client */ public void ping(ByteBuffer data) throws IOException { sendControlMessage(data, Constants23.getOpcodePing()); } /** * Generic function to send either a ping or a pong. * * @param data Optional message. * @param opcode The byte to include as the opcode. * @throws IOException If an error occurs writing to the client */ private void sendControlMessage(ByteBuffer data, byte opcode) throws IOException { try { synchronized (stateLock) { if (closed) { throw new IOException(sm.getString("outbound.closed")); } doFlush(false); upgradeOutbound.write(0x80 | opcode); if (data == null) { upgradeOutbound.write(0); } else { upgradeOutbound.write(data.limit() - data.position()); upgradeOutbound.write(data.array(), data.position(), data.limit() - data.position()); } upgradeOutbound.flush(); } } catch (IOException ioe) { // Any IOException is terminal. Make sure the Inbound side knows // that something went wrong. // The exception handling needs to be outside of the sync to avoid // possible deadlocks (e.g. BZ55524) when triggering the inbound // close as that will execute user code streamInbound.doOnClose(Constants23.getStatusClosedUnexpectedly()); throw ioe; } } /** * Writes the provided bytes as the payload in a new WebSocket frame. * * @param buffer The bytes to include in the payload. * @param finalFragment Do these bytes represent the final fragment of a WebSocket message? * @throws IOException */ private void doWriteBytes(ByteBuffer buffer, boolean finalFragment) throws IOException { if (closed) { throw new IOException(sm.getString("outbound.closed")); } // Work out the first byte int first = 0x00; if (finalFragment) { first = first + 0x80; } if (firstFrame) { if (text.booleanValue()) { first = first + 0x1; } else { first = first + 0x2; } } // Continuation frame is OpCode 0 upgradeOutbound.write(first); if (buffer.limit() < 126) { upgradeOutbound.write(buffer.limit()); } else if (buffer.limit() < 65536) { upgradeOutbound.write(126); upgradeOutbound.write(buffer.limit() >>> 8); upgradeOutbound.write(buffer.limit() & 0xFF); } else { // Will never be more than 2^31-1 upgradeOutbound.write(127); upgradeOutbound.write(0); upgradeOutbound.write(0); upgradeOutbound.write(0); upgradeOutbound.write(0); upgradeOutbound.write(buffer.limit() >>> 24); upgradeOutbound.write(buffer.limit() >>> 16); upgradeOutbound.write(buffer.limit() >>> 8); upgradeOutbound.write(buffer.limit() & 0xFF); } // Write the content upgradeOutbound.write(buffer.array(), buffer.arrayOffset(), buffer.limit()); upgradeOutbound.flush(); // Reset if (finalFragment) { text = null; firstFrame = true; } else { firstFrame = false; } bb.clear(); } /* * Convert the textual message to bytes and then output it. */ private void doWriteText(CharBuffer buffer, boolean finalFragment) throws IOException { CharsetEncoder encoder = B2CConverter.getUtf8().newEncoder(); do { CoderResult cr = encoder.encode(buffer, bb, true); if (cr.isError()) { cr.throwException(); } bb.flip(); if (buffer.hasRemaining()) { doWriteBytes(bb, false); } else { doWriteBytes(bb, finalFragment); } } while (buffer.hasRemaining()); // Reset - bb will be cleared in doWriteBytes() cb.clear(); } public UpgradeOutbound getUpgradeOutbound() { return upgradeOutbound; } public void setUpgradeOutbound(UpgradeOutbound upgradeOutbound) { this.upgradeOutbound = upgradeOutbound; } public StreamInbound getStreamInbound() { return streamInbound; } public void setStreamInbound(StreamInbound streamInbound) { this.streamInbound = streamInbound; } public ByteBuffer getBb() { return bb; } public void setBb(ByteBuffer bb) { this.bb = bb; } public CharBuffer getCb() { return cb; } public void setCb(CharBuffer cb) { this.cb = cb; } public boolean isClosed() { return closed; } public void setClosed(boolean closed) { this.closed = closed; } public Boolean getText() { return text; } public void setText(Boolean text) { this.text = text; } public boolean isFirstFrame() { return firstFrame; } public void setFirstFrame(boolean firstFrame) { this.firstFrame = firstFrame; } public static StringManager3 getSm() { return sm; } public static int getDefaultBufferSize() { return DEFAULT_BUFFER_SIZE; } public Object getStateLock() { return stateLock; } }