/* * 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 org.apache.catalina.websocket; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import org.apache.coyote.http11.upgrade.UpgradeOutbound; import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.res.StringManager; /** * 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 StringManager sm = StringManager.getManager(Constants.Package); public 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://bz.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 (Boolean.TRUE.equals(text)) { // 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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 (Boolean.FALSE.equals(text)) { // 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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(Constants.STATUS_PROTOCOL_ERROR, null); } } else { // No status close(0, null); } } private boolean validateCloseStatus(int status) { if (status == Constants.STATUS_CLOSE_NORMAL || status == Constants.STATUS_SHUTDOWN || status == Constants.STATUS_PROTOCOL_ERROR || status == Constants.STATUS_UNEXPECTED_DATA_TYPE || status == Constants.STATUS_BAD_DATA || status == Constants.STATUS_POLICY_VIOLATION || status == Constants.STATUS_MESSAGE_TOO_LARGE || status == Constants.STATUS_REQUIRED_EXTENSION || status == Constants.STATUS_UNEXPECTED_CONDITION || (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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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, Constants.OPCODE_PONG); } /** * 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, Constants.OPCODE_PING); } /** * 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(Constants.STATUS_CLOSED_UNEXPECTEDLY); 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.UTF_8.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(); } }