You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2012/03/16 00:17:48 UTC

svn commit: r1301264 - in /tomcat/tc7.0.x/trunk: ./ java/org/apache/catalina/websocket/ java/org/apache/coyote/http11/upgrade/ test/org/apache/catalina/websocket/ webapps/examples/WEB-INF/classes/websocket/

Author: markt
Date: Thu Mar 15 23:17:47 2012
New Revision: 1301264

URL: http://svn.apache.org/viewvc?rev=1301264&view=rev
Log:
More WebSocket

Added:
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java   (contents, props changed)
      - copied, changed from r1293154, tomcat/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrame.java
      - copied, changed from r1292670, tomcat/trunk/java/org/apache/catalina/websocket/WsFrame.java
Removed:
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrameHeader.java
Modified:
    tomcat/tc7.0.x/trunk/   (props changed)
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/LocalStrings.properties
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/MessageInbound.java
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WebSocketServlet.java
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsInputStream.java
    tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsOutbound.java
    tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeAprProcessor.java
    tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeBioProcessor.java
    tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeNioProcessor.java
    tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeOutbound.java
    tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeProcessor.java
    tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java
    tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoMessage.java
    tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoStream.java

Propchange: tomcat/tc7.0.x/trunk/
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Thu Mar 15 23:17:47 2012
@@ -1 +1 @@
-/tomcat/trunk:1156115-1157160,1157162-1157859,1157862-1157942,1157945-1160347,1160349-1163716,1163718-1166689,1166691-1174340,1174342-1175596,1175598-1175611,1175613-1175932,1175934-1177783,1177785-1177980,1178006-1180720,1180722-1183094,1183096-1187753,1187755,1187775,1187801,1187806,1187809,1187826-1188312,1188314-1188401,1188646-1188840,1188842-1190176,1190178-1195223,1195225-1195953,1195955,1195957-1201238,1201240-1203345,1203347-1206623,1206625-1208046,1208073,1208096,1208114,1208145,1208772,1209194-1212125,1212127-1220291,1220293,1220295-1221321,1221323-1222328,1222332-1222401,1222405-1222795,1222850-1222950,1222969-1225326,1225328-1225463,1225465,1225627,1225629-1226534,1226536-1228908,1228911-1228923,1228927-1229532,1229534-1230766,1230768-1231625,1231627-1233414,1233419-1235207,1235209-1237425,1237427,1237429-1237977,1237981,1237985,1237995,1238070,1238073,1239024-1239048,1239050-1239062,1239135,1239256,1239258-1239485,1239785-1240046,1240101,1240106,1240109,1240112
 ,1240114,1240116,1240118,1240121,1240329,1240474-1240850,1240857,1241087,1241160,1241408-1241822,1241908-1241909,1241912-1242110,1242371-1292130,1292334,1292338,1292345-1292347,1293155,1293831-1293832,1295998,1296284,1297014-1297015,1297017,1297158,1297177,1297202,1297209,1297213,1297717,1297722,1297729,1297768,1297778,1297818,1297828,1297979,1297987,1298121,1298140,1298590,1298592,1298628-1298629,1298794,1298983-1298984,1299020,1299034,1299819,1300154-1300155,1300569,1300948
+/tomcat/trunk:1156115-1157160,1157162-1157859,1157862-1157942,1157945-1160347,1160349-1163716,1163718-1166689,1166691-1174340,1174342-1175596,1175598-1175611,1175613-1175932,1175934-1177783,1177785-1177980,1178006-1180720,1180722-1183094,1183096-1187753,1187755,1187775,1187801,1187806,1187809,1187826-1188312,1188314-1188401,1188646-1188840,1188842-1190176,1190178-1195223,1195225-1195953,1195955,1195957-1201238,1201240-1203345,1203347-1206623,1206625-1208046,1208073,1208096,1208114,1208145,1208772,1209194-1212125,1212127-1220291,1220293,1220295-1221321,1221323-1222328,1222332-1222401,1222405-1222795,1222850-1222950,1222969-1225326,1225328-1225463,1225465,1225627,1225629-1226534,1226536-1228908,1228911-1228923,1228927-1229532,1229534-1230766,1230768-1231625,1231627-1233414,1233419-1235207,1235209-1237425,1237427,1237429-1237977,1237981,1237985,1237995,1238070,1238073,1239024-1239048,1239050-1239062,1239135,1239256,1239258-1239485,1239785-1240046,1240101,1240106,1240109,1240112
 ,1240114,1240116,1240118,1240121,1240329,1240474-1240850,1240857,1241087,1241160,1241408-1241822,1241908-1241909,1241912-1242110,1242371-1292130,1292134-1292458,1292464-1292670,1292672-1292776,1292780-1293392,1293831-1293832,1295998,1296284,1297014-1297015,1297017,1297158,1297177,1297202,1297209,1297213,1297717,1297722,1297729,1297768,1297778,1297818,1297828,1297979,1297987,1298121,1298140,1298590,1298592,1298628-1298629,1298794,1298983-1298984,1299020,1299034,1299819,1300154-1300155,1300569,1300948

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/LocalStrings.properties
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/LocalStrings.properties?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/LocalStrings.properties (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/LocalStrings.properties Thu Mar 15 23:17:47 2012
@@ -11,4 +11,15 @@
 # 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.
\ No newline at end of file
+# limitations under the License.
+
+frame.eos=The end of the stream was reached before the expected number of payload bytes could be read
+frame.invalidUtf8=A sequence of bytes was received that did not represent valid UTF-8
+frame.notMasked=The client frame was not masked but all client frames must be masked
+
+is.notContinutation=A frame with the OpCode [{0}] was recieved when a continuation frame was expected
+is.unknownOpCode=A frame with the unrecognized OpCode [{0}] was received
+
+message.bufferTooSmall=The buffer is not big enough to contain the message currently being processed
+
+outbound.closed=The WebSocket connection has been closed
\ No newline at end of file

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/MessageInbound.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/MessageInbound.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/MessageInbound.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/MessageInbound.java Thu Mar 15 23:17:47 2012
@@ -22,17 +22,30 @@ import java.io.Reader;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Base implementation of the class used to process WebSocket connections based
+ * on messages. Applications should extend this class to provide application
+ * specific functionality. Applications that wish to operate on a stream basis
+ * rather than a message basis should use {@link StreamInbound}.
+ */
 public abstract class MessageInbound extends StreamInbound {
 
+    private static final StringManager sm =
+            StringManager.getManager(Constants.Package);
+
+
     // 2MB - like maxPostSize
     private int byteBufferMaxSize = 2097152;
     private int charBufferMaxSize = 2097152;
 
-    ByteBuffer bb = ByteBuffer.allocate(8192);
-    CharBuffer cb = CharBuffer.allocate(8192);
+    private ByteBuffer bb = ByteBuffer.allocate(8192);
+    private CharBuffer cb = CharBuffer.allocate(8192);
+
 
     @Override
-    protected void onBinaryData(InputStream is) throws IOException {
+    protected final void onBinaryData(InputStream is) throws IOException {
         int read = 0;
         while (read > -1) {
             bb.position(bb.position() + read);
@@ -46,8 +59,9 @@ public abstract class MessageInbound ext
         bb.clear();
     }
 
+
     @Override
-    protected void onTextData(Reader r) throws IOException {
+    protected final void onTextData(Reader r) throws IOException {
         int read = 0;
         while (read > -1) {
             cb.position(cb.position() + read);
@@ -61,11 +75,11 @@ public abstract class MessageInbound ext
         cb.clear();
     }
 
+
     private void resizeByteBuffer() throws IOException {
         int maxSize = getByteBufferMaxSize();
         if (bb.limit() >= maxSize) {
-            // TODO i18n
-            throw new IOException("Buffer not big enough for message");
+            throw new IOException(sm.getString("message.bufferTooSmall"));
         }
 
         long newSize = bb.limit() * 2;
@@ -80,11 +94,11 @@ public abstract class MessageInbound ext
         bb = newBuffer;
     }
 
+
     private void resizeCharBuffer() throws IOException {
         int maxSize = getCharBufferMaxSize();
         if (cb.limit() >= maxSize) {
-            // TODO i18n
-            throw new IOException("Buffer not big enough for message");
+            throw new IOException(sm.getString("message.bufferTooSmall"));
         }
 
         long newSize = cb.limit() * 2;
@@ -99,24 +113,70 @@ public abstract class MessageInbound ext
         cb = newBuffer;
     }
 
-    public int getByteBufferMaxSize() {
+
+    /**
+     * Obtain the current maximum size (in bytes) of the buffer used for binary
+     * messages.
+     */
+    public final int getByteBufferMaxSize() {
         return byteBufferMaxSize;
     }
 
-    public void setByteBufferMaxSize(int byteBufferMaxSize) {
+
+    /**
+     * Set the maximum size (in bytes) of the buffer used for binary messages.
+     */
+    public final void setByteBufferMaxSize(int byteBufferMaxSize) {
         this.byteBufferMaxSize = byteBufferMaxSize;
     }
 
-    public int getCharBufferMaxSize() {
+
+    /**
+     * Obtain the current maximum size (in characters) of the buffer used for
+     * binary messages.
+     */
+    public final int getCharBufferMaxSize() {
         return charBufferMaxSize;
     }
 
-    public void setCharBufferMaxSize(int charBufferMaxSize) {
+
+    /**
+     * Set the maximum size (in characters) of the buffer used for textual
+     * messages.
+     */
+    public final void setCharBufferMaxSize(int charBufferMaxSize) {
         this.charBufferMaxSize = charBufferMaxSize;
     }
 
+
+    /**
+     * This method is called when there is a binary WebSocket message available
+     * to process. The message is presented via a ByteBuffer and may have been
+     * formed from one or more frames. The number of frames used to transmit the
+     * message is not made visible to the application.
+     *
+     * @param message       The WebSocket message
+     *
+     * @throws IOException  If a problem occurs processing the message. Any
+     *                      exception will trigger the closing of the WebSocket
+     *                      connection.
+     */
     protected abstract void onBinaryMessage(ByteBuffer message)
             throws IOException;
+
+
+    /**
+     * This method is called when there is a textual WebSocket message available
+     * to process. The message is presented via a CharBuffer and may have been
+     * formed from one or more frames. The number of frames used to transmit the
+     * message is not made visible to the application.
+     *
+     * @param message       The WebSocket message
+     *
+     * @throws IOException  If a problem occurs processing the message. Any
+     *                      exception will trigger the closing of the WebSocket
+     *                      connection.
+     */
     protected abstract void onTextMessage(CharBuffer message)
             throws IOException;
 }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java Thu Mar 15 23:17:47 2012
@@ -20,126 +20,130 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
-import java.nio.ByteBuffer;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.UnmappableCharacterException;
 
 import org.apache.coyote.http11.upgrade.UpgradeInbound;
 import org.apache.coyote.http11.upgrade.UpgradeOutbound;
 import org.apache.coyote.http11.upgrade.UpgradeProcessor;
-import org.apache.tomcat.util.buf.B2CConverter;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
 
+/**
+ * Base implementation of the class used to process WebSocket connections based
+ * on streams. Applications should extend this class to provide application
+ * specific functionality. Applications that wish to operate on a message basis
+ * rather than a stream basis should use {@link MessageInbound}.
+ */
 public abstract class StreamInbound implements UpgradeInbound {
 
     private UpgradeProcessor<?> processor = null;
     private WsOutbound outbound;
 
     @Override
-    public void setUpgradeOutbound(UpgradeOutbound upgradeOutbound) {
+    public final void setUpgradeOutbound(UpgradeOutbound upgradeOutbound) {
         outbound = new WsOutbound(upgradeOutbound);
     }
 
 
     @Override
-    public void setUpgradeProcessor(UpgradeProcessor<?> processor) {
+    public final void setUpgradeProcessor(UpgradeProcessor<?> processor) {
         this.processor = processor;
     }
 
-    public WsOutbound getOutbound() {
+
+    /**
+     * Obtain the outbound side of this WebSocket connection used for writing
+     * data to the client.
+     */
+    public final WsOutbound getWsOutbound() {
         return outbound;
     }
 
+
     @Override
-    public SocketState onData() throws IOException {
+    public final SocketState onData() throws IOException {
         // Must be start the start of a frame or series of frames
-        WsInputStream wsIs = new WsInputStream(processor);
 
-        WsFrameHeader header = wsIs.getFrameHeader();
+        try {
+            WsInputStream wsIs = new WsInputStream(processor, getWsOutbound());
 
-        // TODO User defined extensions may define values for rsv
-        if (header.getRsv() > 0) {
-            getOutbound().close(1002, null);
-            return SocketState.CLOSED;
-        }
-
-        byte opCode = header.getOpCode();
-
-        if (opCode == Constants.OPCODE_BINARY) {
-            onBinaryData(wsIs);
-            return SocketState.UPGRADED;
-        } else if (opCode == Constants.OPCODE_TEXT) {
-            InputStreamReader r =
-                    new InputStreamReader(wsIs, B2CConverter.UTF_8);
-            onTextData(r);
-            return SocketState.UPGRADED;
-        }
+            WsFrame frame = wsIs.getFrame();
 
-        // Must be a control frame and control frames:
-        // - have a limited payload length
-        // - must not be fragmented
-        if (wsIs.getPayloadLength() > 125 || !wsIs.getFrameHeader().getFin()) {
-            getOutbound().close(1002, null);
-            return SocketState.CLOSED;
-        }
+            // TODO User defined extensions may define values for rsv
+            if (frame.getRsv() > 0) {
+                getWsOutbound().close(1002, null);
+                return SocketState.CLOSED;
+            }
 
-        if (opCode == Constants.OPCODE_CLOSE){
-            doClose(wsIs);
-            return SocketState.CLOSED;
-        } else if (opCode == Constants.OPCODE_PING) {
-            doPing(wsIs);
-            return SocketState.UPGRADED;
-        } else if (opCode == Constants.OPCODE_PONG) {
-            doPong(wsIs);
-            return SocketState.UPGRADED;
-        }
+            byte opCode = frame.getOpCode();
 
-        getOutbound().close(1002, null);
-        return SocketState.CLOSED;
-    }
+            if (opCode == Constants.OPCODE_BINARY) {
+                onBinaryData(wsIs);
+                return SocketState.UPGRADED;
+            } else if (opCode == Constants.OPCODE_TEXT) {
+                InputStreamReader r =
+                        new InputStreamReader(wsIs, new Utf8Decoder());
+                onTextData(r);
+                return SocketState.UPGRADED;
+            }
 
-    private void doClose(InputStream is) throws IOException {
-        // Control messages have a max size of 125 bytes. Need to try and read
-        // one more so we reach end of stream (less 2 for the status)
-        ByteBuffer data = ByteBuffer.allocate(124);
-
-        int status = is.read();
-        if (status != -1) {
-            status = status << 8;
-            status = status + is.read();
-            int read = 0;
-            while (read > -1) {
-                data.position(data.position() + read);
-                read = is.read(data.array(), data.position(), data.remaining());
+            if (opCode == Constants.OPCODE_CLOSE){
+                getWsOutbound().close(frame);
+                return SocketState.CLOSED;
+            } else if (opCode == Constants.OPCODE_PING) {
+                getWsOutbound().pong(frame.getPayLoad());
+                return SocketState.UPGRADED;
+            } else if (opCode == Constants.OPCODE_PONG) {
+                // NO-OP
+                return SocketState.UPGRADED;
             }
-        } else {
-            status = 0;
-        }
-        data.flip();
-        getOutbound().close(status, data);
-    }
 
-    private void doPing(InputStream is) throws IOException {
-        // Control messages have a max size of 125 bytes. Need to try and read
-        // one more so we reach end of stream
-        ByteBuffer data = ByteBuffer.allocate(126);
-
-        int read = 0;
-        while (read > -1) {
-            data.position(data.position() + read);
-            read = is.read(data.array(), data.position(), data.remaining());
+            // Unknown OpCode
+            getWsOutbound().close(1002, null);
+            return SocketState.CLOSED;
+        } catch (MalformedInputException mie) {
+            // Invalid UTF-8
+            getWsOutbound().close(1007, null);
+            return SocketState.CLOSED;
+        } catch (UnmappableCharacterException uce) {
+            // Invalid UTF-8
+            getWsOutbound().close(1007, null);
+            return SocketState.CLOSED;
+        } catch (IOException ioe) {
+            // Given something must have gone to reach this point, this might
+            // not work but try it anyway.
+            getWsOutbound().close(1002, null);
+            return SocketState.CLOSED;
         }
-
-        data.flip();
-        getOutbound().pong(data);
     }
 
-    private void doPong(InputStream is) throws IOException {
-        // Unsolicited pong - swallow it
-        int read = 0;
-        while (read > -1) {
-            read = is.read();
-        }
-    }
 
+    /**
+     * This method is called when there is a binary WebSocket message available
+     * to process. The message is presented via a stream and may be formed from
+     * one or more frames. The number of frames used to transmit the message is
+     * not made visible to the application.
+     *
+     * @param is    The WebSocket message
+     *
+     * @throws IOException  If a problem occurs processing the message. Any
+     *                      exception will trigger the closing of the WebSocket
+     *                      connection.
+     */
     protected abstract void onBinaryData(InputStream is) throws IOException;
+
+
+    /**
+     * This method is called when there is a textual WebSocket message available
+     * to process. The message is presented via a reader and may be formed from
+     * one or more frames. The number of frames used to transmit the message is
+     * not made visible to the application.
+     *
+     * @param r     The WebSocket message
+     *
+     * @throws IOException  If a problem occurs processing the message. Any
+     *                      exception will trigger the closing of the WebSocket
+     *                      connection.
+     */
     protected abstract void onTextData(Reader r) throws IOException;
 }

Copied: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java (from r1293154, tomcat/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java)
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java?p2=tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java&p1=tomcat/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java&r1=1293154&r2=1301264&rev=1301264&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java Thu Mar 15 23:17:47 2012
@@ -1,207 +1,207 @@
-/*
- * 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.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CoderResult;
-
-import org.apache.tomcat.util.buf.B2CConverter;
-
-/**
- * Decodes bytes to UTF-8. Extracted from Apache Harmony and modified to reject
- * code points from U+D800 to U+DFFF as per RFC3629. The standard Java decoder
- * does not reject these.
- */
-public class Utf8Decoder extends CharsetDecoder {
-
-    // The next table contains information about UTF-8 charset and
-    // correspondence of 1st byte to the length of sequence
-    // For information please visit http://www.ietf.org/rfc/rfc3629.txt
-    //
-    // Please note, o means 0, actually.
-    // -------------------------------------------------------------------
-    // 0         1         2         3          Value
-    // -------------------------------------------------------------------
-    // oxxxxxxx                                 00000000 00000000 0xxxxxxx
-    // 11oyyyyy  1oxxxxxx                       00000000 00000yyy yyxxxxxx
-    // 111ozzzz  1oyyyyyy  1oxxxxxx             00000000 zzzzyyyy yyxxxxxx
-    // 1111ouuu  1ouuzzzz  1oyyyyyy  1oxxxxxx   000uuuuu zzzzyyyy yyxxxxxx
-
-    private static final int remainingBytes[] = {
-            // 1owwwwww
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            // 11oyyyyy
-            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-            1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-            // 111ozzzz
-            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
-            // 1111ouuu
-            3, 3, 3, 3, 3, 3, 3, 3,
-            // > 11110111
-            -1, -1, -1, -1, -1, -1, -1, -1 };
-
-    private static final int remainingNumbers[] = {
-                   0, //                0                 1                2           3
-                4224, // (01o00000b <<  6)+(1o000000b)
-              401536, // (011o0000b << 12)+(1o000000b <<  6)+(1o000000b)
-            29892736  // (0111o000b << 18)+(1o000000b << 12)+(1o000000b << 6)+(1o000000b)
-    };
-
-    private static final int lowerEncodingLimit[] = { -1, 0x80, 0x800, 0x10000 };
-
-    public Utf8Decoder() {
-        super(B2CConverter.UTF_8, 1.0f, 1.0f);
-    }
-
-    @Override
-    protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
-        if (in.hasArray() && out.hasArray()) {
-            return decodeHasArray(in, out);
-        }
-        return decodeNotHasArray(in, out);
-    }
-
-    private CoderResult decodeNotHasArray(ByteBuffer in, CharBuffer out) {
-        int outRemaining = out.remaining();
-        int pos = in.position();
-        int limit = in.limit();
-        try {
-            while (pos < limit) {
-                if (outRemaining == 0) {
-                    return CoderResult.OVERFLOW;
-                }
-
-                int jchar = in.get();
-                if (jchar < 0) {
-                    jchar = jchar & 0x7F;
-                    int tail = remainingBytes[jchar];
-                    if (tail == -1) {
-                        return CoderResult.malformedForLength(1);
-                    }
-                    if (limit - pos < 1 + tail) {
-                        return CoderResult.UNDERFLOW;
-                    }
-
-                    int nextByte;
-                    for (int i = 0; i < tail; i++) {
-                        nextByte = in.get() & 0xFF;
-                        if ((nextByte & 0xC0) != 0x80) {
-                            return CoderResult
-                                    .malformedForLength(1 + i);
-                        }
-                        jchar = (jchar << 6) + nextByte;
-                    }
-                    jchar -= remainingNumbers[tail];
-                    if (jchar < lowerEncodingLimit[tail]) {
-                        // Should have been encoded in a fewer octets
-                        return CoderResult.malformedForLength(1);
-                    }
-                    pos += tail;
-                }
-                if (jchar <= 0xffff) {
-                  out.put((char) jchar);
-                  outRemaining--;
-                } else {
-                  if (outRemaining < 2) {
-                      return CoderResult.OVERFLOW;
-                  }
-                  out.put((char) ((jchar >> 0xA) + 0xD7C0));
-                  out.put((char) ((jchar & 0x3FF) + 0xDC00));
-                  outRemaining -= 2;
-                }
-                pos++;
-            }
-            return CoderResult.UNDERFLOW;
-        } finally {
-            in.position(pos);
-        }
-    }
-
-    private CoderResult decodeHasArray(ByteBuffer in, CharBuffer out) {
-        int outRemaining = out.remaining();
-        int pos = in.position();
-        int limit = in.limit();
-        final byte[] bArr = in.array();
-        final char[] cArr = out.array();
-        final int inIndexLimit = limit + in.arrayOffset();
-
-        int inIndex = pos + in.arrayOffset();
-        int outIndex = out.position() + out.arrayOffset();
-
-        // if someone would change the limit in process,
-        // he would face consequences
-        for (; inIndex < inIndexLimit && outRemaining > 0; inIndex++) {
-            int jchar = bArr[inIndex];
-            if (jchar < 0) {
-                jchar = jchar & 0x7F;
-                int tail = remainingBytes[jchar];
-
-                if (tail == -1) {
-                    in.position(inIndex - in.arrayOffset());
-                    out.position(outIndex - out.arrayOffset());
-                    return CoderResult.malformedForLength(1);
-                }
-                if (inIndexLimit - inIndex < 1 + tail) {
-                    break;
-                }
-
-                for (int i = 0; i < tail; i++) {
-                    int nextByte = bArr[inIndex + i + 1] & 0xFF;
-                    if ((nextByte & 0xC0) != 0x80) {
-                        in.position(inIndex - in.arrayOffset());
-                        out.position(outIndex - out.arrayOffset());
-                        return CoderResult.malformedForLength(1 + i);
-                    }
-                    jchar = (jchar << 6) + nextByte;
-                }
-                jchar -= remainingNumbers[tail];
-                if (jchar < lowerEncodingLimit[tail]) {
-                    // Should have been encoded in fewer octets
-                    in.position(inIndex - in.arrayOffset());
-                    out.position(outIndex - out.arrayOffset());
-                    return CoderResult.malformedForLength(1);
-                }
-                inIndex += tail;
-            }
-            // Note: This is the additional test added
-            if (jchar >= 0xD800 && jchar <=0xDFFF) {
-                return CoderResult.unmappableForLength(3);
-            }
-            if (jchar <= 0xffff) {
-              cArr[outIndex++] = (char) jchar;
-              outRemaining--;
-            } else {
-              if (outRemaining < 2) {
-                  return CoderResult.OVERFLOW;
-              }
-              cArr[outIndex++] = (char) ((jchar >> 0xA) + 0xD7C0);
-              cArr[outIndex++] = (char) ((jchar & 0x3FF) + 0xDC00);
-              outRemaining -= 2;
-            }
-        }
-        in.position(inIndex - in.arrayOffset());
-        out.position(outIndex - out.arrayOffset());
-        return (outRemaining == 0 && inIndex < inIndexLimit) ?
-                CoderResult.OVERFLOW :
-                CoderResult.UNDERFLOW;
-    }
-}
+/*
+ * 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.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+
+import org.apache.tomcat.util.buf.B2CConverter;
+
+/**
+ * Decodes bytes to UTF-8. Extracted from Apache Harmony and modified to reject
+ * code points from U+D800 to U+DFFF as per RFC3629. The standard Java decoder
+ * does not reject these.
+ */
+public class Utf8Decoder extends CharsetDecoder {
+
+    // The next table contains information about UTF-8 charset and
+    // correspondence of 1st byte to the length of sequence
+    // For information please visit http://www.ietf.org/rfc/rfc3629.txt
+    //
+    // Please note, o means 0, actually.
+    // -------------------------------------------------------------------
+    // 0         1         2         3          Value
+    // -------------------------------------------------------------------
+    // oxxxxxxx                                 00000000 00000000 0xxxxxxx
+    // 11oyyyyy  1oxxxxxx                       00000000 00000yyy yyxxxxxx
+    // 111ozzzz  1oyyyyyy  1oxxxxxx             00000000 zzzzyyyy yyxxxxxx
+    // 1111ouuu  1ouuzzzz  1oyyyyyy  1oxxxxxx   000uuuuu zzzzyyyy yyxxxxxx
+
+    private static final int remainingBytes[] = {
+            // 1owwwwww
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            // 11oyyyyy
+            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+            1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+            // 111ozzzz
+            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+            // 1111ouuu
+            3, 3, 3, 3, 3, 3, 3, 3,
+            // > 11110111
+            -1, -1, -1, -1, -1, -1, -1, -1 };
+
+    private static final int remainingNumbers[] = {
+                   0, //                0                 1                2           3
+                4224, // (01o00000b <<  6)+(1o000000b)
+              401536, // (011o0000b << 12)+(1o000000b <<  6)+(1o000000b)
+            29892736  // (0111o000b << 18)+(1o000000b << 12)+(1o000000b << 6)+(1o000000b)
+    };
+
+    private static final int lowerEncodingLimit[] = { -1, 0x80, 0x800, 0x10000 };
+
+    public Utf8Decoder() {
+        super(B2CConverter.UTF_8, 1.0f, 1.0f);
+    }
+
+    @Override
+    protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
+        if (in.hasArray() && out.hasArray()) {
+            return decodeHasArray(in, out);
+        }
+        return decodeNotHasArray(in, out);
+    }
+
+    private CoderResult decodeNotHasArray(ByteBuffer in, CharBuffer out) {
+        int outRemaining = out.remaining();
+        int pos = in.position();
+        int limit = in.limit();
+        try {
+            while (pos < limit) {
+                if (outRemaining == 0) {
+                    return CoderResult.OVERFLOW;
+                }
+
+                int jchar = in.get();
+                if (jchar < 0) {
+                    jchar = jchar & 0x7F;
+                    int tail = remainingBytes[jchar];
+                    if (tail == -1) {
+                        return CoderResult.malformedForLength(1);
+                    }
+                    if (limit - pos < 1 + tail) {
+                        return CoderResult.UNDERFLOW;
+                    }
+
+                    int nextByte;
+                    for (int i = 0; i < tail; i++) {
+                        nextByte = in.get() & 0xFF;
+                        if ((nextByte & 0xC0) != 0x80) {
+                            return CoderResult
+                                    .malformedForLength(1 + i);
+                        }
+                        jchar = (jchar << 6) + nextByte;
+                    }
+                    jchar -= remainingNumbers[tail];
+                    if (jchar < lowerEncodingLimit[tail]) {
+                        // Should have been encoded in a fewer octets
+                        return CoderResult.malformedForLength(1);
+                    }
+                    pos += tail;
+                }
+                if (jchar <= 0xffff) {
+                  out.put((char) jchar);
+                  outRemaining--;
+                } else {
+                  if (outRemaining < 2) {
+                      return CoderResult.OVERFLOW;
+                  }
+                  out.put((char) ((jchar >> 0xA) + 0xD7C0));
+                  out.put((char) ((jchar & 0x3FF) + 0xDC00));
+                  outRemaining -= 2;
+                }
+                pos++;
+            }
+            return CoderResult.UNDERFLOW;
+        } finally {
+            in.position(pos);
+        }
+    }
+
+    private CoderResult decodeHasArray(ByteBuffer in, CharBuffer out) {
+        int outRemaining = out.remaining();
+        int pos = in.position();
+        int limit = in.limit();
+        final byte[] bArr = in.array();
+        final char[] cArr = out.array();
+        final int inIndexLimit = limit + in.arrayOffset();
+
+        int inIndex = pos + in.arrayOffset();
+        int outIndex = out.position() + out.arrayOffset();
+
+        // if someone would change the limit in process,
+        // he would face consequences
+        for (; inIndex < inIndexLimit && outRemaining > 0; inIndex++) {
+            int jchar = bArr[inIndex];
+            if (jchar < 0) {
+                jchar = jchar & 0x7F;
+                int tail = remainingBytes[jchar];
+
+                if (tail == -1) {
+                    in.position(inIndex - in.arrayOffset());
+                    out.position(outIndex - out.arrayOffset());
+                    return CoderResult.malformedForLength(1);
+                }
+                if (inIndexLimit - inIndex < 1 + tail) {
+                    break;
+                }
+
+                for (int i = 0; i < tail; i++) {
+                    int nextByte = bArr[inIndex + i + 1] & 0xFF;
+                    if ((nextByte & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1 + i);
+                    }
+                    jchar = (jchar << 6) + nextByte;
+                }
+                jchar -= remainingNumbers[tail];
+                if (jchar < lowerEncodingLimit[tail]) {
+                    // Should have been encoded in fewer octets
+                    in.position(inIndex - in.arrayOffset());
+                    out.position(outIndex - out.arrayOffset());
+                    return CoderResult.malformedForLength(1);
+                }
+                inIndex += tail;
+            }
+            // Note: This is the additional test added
+            if (jchar >= 0xD800 && jchar <=0xDFFF) {
+                return CoderResult.unmappableForLength(3);
+            }
+            if (jchar <= 0xffff) {
+              cArr[outIndex++] = (char) jchar;
+              outRemaining--;
+            } else {
+              if (outRemaining < 2) {
+                  return CoderResult.OVERFLOW;
+              }
+              cArr[outIndex++] = (char) ((jchar >> 0xA) + 0xD7C0);
+              cArr[outIndex++] = (char) ((jchar & 0x3FF) + 0xDC00);
+              outRemaining -= 2;
+            }
+        }
+        in.position(inIndex - in.arrayOffset());
+        out.position(outIndex - out.arrayOffset());
+        return (outRemaining == 0 && inIndex < inIndexLimit) ?
+                CoderResult.OVERFLOW :
+                CoderResult.UNDERFLOW;
+    }
+}

Propchange: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/Utf8Decoder.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WebSocketServlet.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WebSocketServlet.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WebSocketServlet.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WebSocketServlet.java Thu Mar 15 23:17:47 2012
@@ -175,6 +175,7 @@ public abstract class WebSocketServlet e
         }
     }
 
+
     /**
      * Intended to be overridden by sub-classes that wish to verify the origin
      * of a WebSocket request before processing it.
@@ -190,6 +191,7 @@ public abstract class WebSocketServlet e
         return true;
     }
 
+
     /**
      * Intended to be overridden by sub-classes that wish to select a
      * sub-protocol if the client provides a list of supported protocols.
@@ -199,14 +201,17 @@ public abstract class WebSocketServlet e
      *                      obligation to respect the declared preference
      * @return  <code>null</code> if no sub-protocol is selected or the name of
      *          the protocol which <b>must</b> be one of the protocols listed by
-     *          the client.
+     *          the client. This default implementation always returns
+     *          <code>null</code>.
      */
     protected String selectSubProtocol(List<String> subProtocols) {
         return null;
     }
 
+
     /**
      * Create the instance that will process this inbound connection.
+     * Applications must provide a new instance for each connection.
      *
      * @param subProtocol   The sub-protocol agreed between the client and
      *                      server or <code>null</code> if none was agreed

Copied: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrame.java (from r1292670, tomcat/trunk/java/org/apache/catalina/websocket/WsFrame.java)
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrame.java?p2=tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrame.java&p1=tomcat/trunk/java/org/apache/catalina/websocket/WsFrame.java&r1=1292670&r2=1301264&rev=1301264&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/websocket/WsFrame.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsFrame.java Thu Mar 15 23:17:47 2012
@@ -18,23 +18,41 @@ package org.apache.catalina.websocket;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CoderResult;
 
 import org.apache.catalina.util.Conversions;
 import org.apache.coyote.http11.upgrade.UpgradeProcessor;
+import org.apache.tomcat.util.res.StringManager;
 
 /**
- * Represents a WebSocket frame with the exception of the payload for
+ * Represents a complete WebSocket frame with the exception of the payload for
  * non-control frames.
  */
 public class WsFrame {
 
+    private static final StringManager sm =
+            StringManager.getManager(Constants.Package);
+
+
     private final boolean fin;
     private final int rsv;
     private final byte opCode;
-    private int[] mask = new int[4];
+    private byte[] mask = new byte[4];
     private long payloadLength;
     private ByteBuffer payload;
 
+    /**
+     * Create the new WebSocket frame, reading data from the processor as
+     * necessary.
+     *
+     * @param processor Processor associated with the WebSocket connection on
+     *                  which the frame has been sent
+     *
+     * @throws IOException  If a problem occurs processing the frame. Any
+     *                      exception will trigger the closing of the WebSocket
+     *                      connection.
+     */
     public WsFrame(UpgradeProcessor<?> processor) throws IOException {
 
         int b = processorRead(processor);
@@ -45,8 +63,7 @@ public class WsFrame {
         b = processorRead(processor);
         // Client data must be masked
         if ((b & 0x80) == 0) {
-            // TODO: StringManager / i18n
-            throw new IOException("Client frame not masked");
+            throw new IOException(sm.getString("frame.notMasked"));
         }
 
         payloadLength = b & 0x7F;
@@ -69,14 +86,24 @@ public class WsFrame {
             }
         }
 
-        for (int j = 0; j < mask.length; j++) {
-            mask[j] = processorRead(processor) & 0xFF;
-        }
+        processorRead(processor, mask);
 
         if (isControl()) {
             // Note: Payload limited to <= 125 bytes by test above
             payload = ByteBuffer.allocate((int) payloadLength);
             processorRead(processor, payload);
+
+            if (opCode == Constants.OPCODE_CLOSE && payloadLength > 2) {
+                // Check close payload - if present - is valid UTF-8
+                CharBuffer cb = CharBuffer.allocate((int) payloadLength);
+                Utf8Decoder decoder = new Utf8Decoder();
+                payload.position(2);
+                CoderResult cr = decoder.decode(payload, cb, true);
+                payload.position(0);
+                if (cr.isError()) {
+                    throw new IOException(sm.getString("frame.invalidUtf8"));
+                }
+            }
         } else {
             payload = null;
         }
@@ -98,7 +125,7 @@ public class WsFrame {
         return (opCode & 0x08) > 0;
     }
 
-    public int[] getMask() {
+    public byte[] getMask() {
         return mask;
     }
 
@@ -117,8 +144,7 @@ public class WsFrame {
             throws IOException {
         int result = processor.read();
         if (result == -1) {
-            // TODO i18n
-            throw new IOException("End of stream before end of frame");
+            throw new IOException(sm.getString("frame.eos"));
         }
         return result;
     }
@@ -131,8 +157,7 @@ public class WsFrame {
         while (read < bytes.length) {
             last = processor.read(bytes, read, bytes.length - read);
             if (last == -1) {
-                // TODO i18n
-                throw new IOException("End of stream before end of frame");
+                throw new IOException(sm.getString("frame.eos"));
             }
             read += last;
         }
@@ -148,8 +173,7 @@ public class WsFrame {
         while (bb.hasRemaining()) {
             last = processor.read();
             if (last == -1) {
-                // TODO i18n
-                throw new IOException("End of stream before end of frame");
+                throw new IOException(sm.getString("frame.eos"));
             }
             bb.put((byte) (last ^ mask[bb.position() % 4]));
         }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsInputStream.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsInputStream.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsInputStream.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsInputStream.java Thu Mar 15 23:17:47 2012
@@ -18,68 +18,48 @@ package org.apache.catalina.websocket;
 
 import java.io.IOException;
 
-import org.apache.catalina.util.Conversions;
 import org.apache.coyote.http11.upgrade.UpgradeProcessor;
+import org.apache.tomcat.util.res.StringManager;
 
+/**
+ * This class is used to read WebSocket frames from the underlying socket and
+ * makes the payload available for reading as an {@link InputStream}. It only
+ * makes the number of bytes declared in the payload length available for
+ * reading even if more bytes are available from the socket.
+ */
 public class WsInputStream extends java.io.InputStream {
 
-    private UpgradeProcessor<?> processor;
-    private WsFrameHeader wsFrameHeader;
-    private long payloadLength = -1;
-    private int[] mask = new int[4];
+    private static final StringManager sm =
+            StringManager.getManager(Constants.Package);
+
 
+    private UpgradeProcessor<?> processor;
+    private WsOutbound outbound;
 
+    private WsFrame frame;
     private long remaining;
     private long readThisFragment;
 
-    public WsInputStream(UpgradeProcessor<?> processor) throws IOException {
-        this.processor = processor;
-
-        processFrameHeader();
-    }
+    private String error = null;
 
 
-    private void processFrameHeader() throws IOException {
-
-        // TODO: Per frame extension handling is not currently supported.
-
-        // TODO: Handle control frames between fragments
-
-        int i = processor.read();
-        this.wsFrameHeader = new WsFrameHeader(i);
-
-        // Client data must be masked
-        i = processor.read();
-        if ((i & 0x80) == 0) {
-            // TODO: StringManager / i18n
-            throw new IOException("Client frame not masked");
-        }
-
-        payloadLength = i & 0x7F;
-        if (payloadLength == 126) {
-            byte[] extended = new byte[2];
-            processor.read(extended);
-            payloadLength = Conversions.byteArrayToLong(extended);
-        } else if (payloadLength == 127) {
-            byte[] extended = new byte[8];
-            processor.read(extended);
-            payloadLength = Conversions.byteArrayToLong(extended);
-        }
-        remaining = payloadLength;
+    public WsInputStream(UpgradeProcessor<?> processor, WsOutbound outbound)
+            throws IOException {
+        this.processor = processor;
+        this.outbound = outbound;
+        processFrame();
+    }
 
-        for (int j = 0; j < mask.length; j++) {
-            mask[j] = processor.read() & 0xFF;
-        }
 
-        readThisFragment = 0;
+    public WsFrame getFrame() {
+        return frame;
     }
 
-    public WsFrameHeader getFrameHeader() {
-        return wsFrameHeader;
-    }
 
-    public long getPayloadLength() {
-        return payloadLength;
+    private void processFrame() throws IOException {
+        frame = new WsFrame(processor);
+        readThisFragment = 0;
+        remaining = frame.getPayLoadLength();
     }
 
 
@@ -87,13 +67,29 @@ public class WsInputStream extends java.
 
     @Override
     public int read() throws IOException {
-        while (remaining == 0 && !getFrameHeader().getFin()) {
+        if (error != null) {
+            throw new IOException(error);
+        }
+        while (remaining == 0 && !getFrame().getFin()) {
             // Need more data - process next frame
-            processFrameHeader();
-
-            if (getFrameHeader().getOpCode() != Constants.OPCODE_CONTINUATION) {
-                // TODO i18n
-                throw new IOException("Not a continuation frame");
+            processFrame();
+            while (frame.isControl()) {
+                if (getFrame().getOpCode() == Constants.OPCODE_PING) {
+                    outbound.pong(frame.getPayLoad());
+                } else if (getFrame().getOpCode() == Constants.OPCODE_PONG) {
+                    // NO-OP. Swallow it.
+                } else if (getFrame().getOpCode() == Constants.OPCODE_CLOSE) {
+                    outbound.close(frame);
+                } else{
+                    throw new IOException(sm.getString("is.unknownOpCode",
+                            Byte.valueOf(getFrame().getOpCode())));
+                }
+                processFrame();
+            }
+            if (getFrame().getOpCode() != Constants.OPCODE_CONTINUATION) {
+                error = sm.getString("is.notContinutation",
+                        Byte.valueOf(getFrame().getOpCode()));
+                throw new IOException(error);
             }
         }
 
@@ -108,6 +104,58 @@ public class WsInputStream extends java.
         if(masked == -1) {
             return -1;
         }
-        return masked ^ mask[(int) ((readThisFragment - 1) % 4)];
+        return masked ^
+                (frame.getMask()[(int) ((readThisFragment - 1) % 4)] & 0xFF);
+    }
+
+
+    @Override
+    public int read(byte b[], int off, int len) throws IOException {
+        if (error != null) {
+            throw new IOException(error);
+        }
+        while (remaining == 0 && !getFrame().getFin()) {
+            // Need more data - process next frame
+            processFrame();
+            while (frame.isControl()) {
+                if (getFrame().getOpCode() == Constants.OPCODE_PING) {
+                    outbound.pong(frame.getPayLoad());
+                } else if (getFrame().getOpCode() == Constants.OPCODE_PONG) {
+                    // NO-OP. Swallow it.
+                } else if (getFrame().getOpCode() == Constants.OPCODE_CLOSE) {
+                    outbound.close(frame);
+                } else{
+                    throw new IOException(sm.getString("is.unknownOpCode",
+                            Byte.valueOf(getFrame().getOpCode())));
+                }
+                processFrame();
+            }
+            if (getFrame().getOpCode() != Constants.OPCODE_CONTINUATION) {
+                error = sm.getString("is.notContinutation",
+                        Byte.valueOf(getFrame().getOpCode()));
+                throw new IOException(error);
+            }
+        }
+
+        if (remaining == 0) {
+            return -1;
+        }
+
+        if (len > remaining) {
+            len = (int) remaining;
+        }
+        int result = processor.read(b, off, len);
+        if(result == -1) {
+            return -1;
+        }
+
+        for (int i = off; i < off + result; i++) {
+            b[i] = (byte) (b[i] ^
+                    frame.getMask()[(int) ((readThisFragment + i - off) % 4)]);
+        }
+        remaining -= result;
+        readThisFragment += result;
+        return result;
     }
+
 }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsOutbound.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsOutbound.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsOutbound.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/WsOutbound.java Thu Mar 15 23:17:47 2012
@@ -19,20 +19,28 @@ 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.
+ */
 public class WsOutbound {
 
+    private static final StringManager sm =
+            StringManager.getManager(Constants.Package);
     private static final int DEFAULT_BUFFER_SIZE = 8192;
 
     private UpgradeOutbound upgradeOutbound;
     private ByteBuffer bb;
     private CharBuffer cb;
     private boolean closed = false;
-    protected Boolean text = null;
-    protected boolean firstFrame = true;
+    private Boolean text = null;
+    private boolean firstFrame = true;
 
 
     public WsOutbound(UpgradeOutbound upgradeOutbound) {
@@ -43,7 +51,23 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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 {
+        if (closed) {
+            throw new IOException(sm.getString("outbound.closed"));
+        }
+
         if (bb.position() == bb.capacity()) {
             doFlush(false);
         }
@@ -58,7 +82,22 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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 b 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 {
+        if (closed) {
+            throw new IOException(sm.getString("outbound.closed"));
+        }
+
         if (cb.position() == cb.capacity()) {
             doFlush(false);
         }
@@ -74,7 +113,20 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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 {
+        if (closed) {
+            throw new IOException(sm.getString("outbound.closed"));
+        }
+
         if (text != null) {
             // Empty the buffer
             flush();
@@ -84,7 +136,20 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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 msgBb The buffer containing the payload
+     *
+     * @throws IOException  If an error occurs writing to the client
+     */
     public void writeTextMessage(CharBuffer msgCb) throws IOException {
+        if (closed) {
+            throw new IOException(sm.getString("outbound.closed"));
+        }
+
         if (text != null) {
             // Empty the buffer
             flush();
@@ -94,10 +159,19 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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 {
+        if (closed) {
+            throw new IOException(sm.getString("outbound.closed"));
+        }
         doFlush(true);
     }
 
+
     private void doFlush(boolean finalFragment) throws IOException {
         if (text == null) {
             // No data
@@ -113,6 +187,64 @@ public class WsOutbound {
     }
 
 
+    /**
+     * 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(1002, null);
+            }
+        } else {
+            // No status
+            close(0, null);
+        }
+    }
+
+
+    private boolean validateCloseStatus(int status) {
+
+        if (status == 1000 || status == 1001 || status == 1002 ||
+                status == 1003 || status == 1007 || status == 1008 ||
+                status == 1009 || status == 1010 || status == 1011 ||
+                (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 {
         // TODO Think about threading requirements for writing. This is not
         // currently thread safe and writing almost certainly needs to be.
@@ -121,20 +253,19 @@ public class WsOutbound {
         }
         closed = true;
 
-        doFlush(true);
-
         upgradeOutbound.write(0x88);
         if (status == 0) {
             upgradeOutbound.write(0);
-        } else if (data == null) {
+        } else if (data == null || data.position() == data.limit()) {
             upgradeOutbound.write(2);
             upgradeOutbound.write(status >>> 8);
             upgradeOutbound.write(status);
         } else {
-            upgradeOutbound.write(2 + data.limit());
+            upgradeOutbound.write(2 + data.limit() - data.position());
             upgradeOutbound.write(status >>> 8);
             upgradeOutbound.write(status);
-            upgradeOutbound.write(data.array(), 0, data.limit());
+            upgradeOutbound.write(data.array(), data.position(),
+                    data.limit() - data.position());
         }
         upgradeOutbound.flush();
 
@@ -143,22 +274,36 @@ public class WsOutbound {
         upgradeOutbound = null;
     }
 
+
+    /**
+     * 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 {
         // TODO Think about threading requirements for writing. This is not
         // currently thread safe and writing almost certainly needs to be.
         if (closed) {
-            // TODO - handle this - ISE?
+            throw new IOException(sm.getString("outbound.closed"));
         }
 
         doFlush(true);
 
         upgradeOutbound.write(0x8A);
-        upgradeOutbound.write(data.limit());
-        upgradeOutbound.write(data.array(), 0, data.limit());
+        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();
     }
 
+
     /**
      * Writes the provided bytes as the payload in a new WebSocket frame.
      *
@@ -170,6 +315,10 @@ public class WsOutbound {
     private void doWriteBytes(ByteBuffer buffer, boolean finalFragment)
             throws IOException {
 
+        if (closed) {
+            throw new IOException("Closed");
+        }
+
         // Work out the first byte
         int first = 0x00;
         if (finalFragment) {
@@ -220,10 +369,17 @@ public class WsOutbound {
     }
 
 
+    /*
+     * 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 {
-            B2CConverter.UTF_8.newEncoder().encode(buffer, bb, true);
+            CoderResult cr = encoder.encode(buffer, bb, true);
+            if (cr.isError()) {
+                cr.throwException();
+            }
             bb.flip();
             if (buffer.hasRemaining()) {
                 doWriteBytes(bb, false);

Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeAprProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeAprProcessor.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeAprProcessor.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeAprProcessor.java Thu Mar 15 23:17:47 2012
@@ -55,6 +55,12 @@ public class UpgradeAprProcessor extends
     }
 
 
+    @Override
+    public void write(byte[]b, int off, int len) throws IOException {
+        Socket.send(socket, b, off, len);
+    }
+
+
     /*
      * Input methods
      */
@@ -67,7 +73,7 @@ public class UpgradeAprProcessor extends
 
 
     @Override
-    public int read(byte[] bytes) throws IOException {
-        return Socket.recv(socket, bytes, 0, bytes.length);
+    public int read(byte[] bytes, int off, int len) throws IOException {
+        return Socket.recv(socket, bytes, off, len);
     }
 }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeBioProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeBioProcessor.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeBioProcessor.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeBioProcessor.java Thu Mar 15 23:17:47 2012
@@ -58,6 +58,12 @@ public class UpgradeBioProcessor extends
     }
 
 
+    @Override
+    public void write(byte[]b, int off, int len) throws IOException {
+        outputStream.write(b, off, len);
+    }
+
+
     /*
      * Input methods
      */
@@ -68,7 +74,7 @@ public class UpgradeBioProcessor extends
 
 
     @Override
-    public int read(byte[] bytes) throws IOException {
-        return inputStream.read(bytes);
+    public int read(byte[] bytes, int off, int len) throws IOException {
+        return inputStream.read(bytes, off, len);
     }
 }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeNioProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeNioProcessor.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeNioProcessor.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeNioProcessor.java Thu Mar 15 23:17:47 2012
@@ -77,7 +77,12 @@ public class UpgradeNioProcessor extends
 
     @Override
     public void write(int b) throws IOException {
-        writeToSocket(new byte[] {(byte) b});
+        writeToSocket(new byte[] {(byte) b}, 0, 1);
+    }
+
+    @Override
+    public void write(byte[]b, int off, int len) throws IOException {
+        writeToSocket(b, off, len);
     }
 
     /*
@@ -91,8 +96,8 @@ public class UpgradeNioProcessor extends
     }
 
     @Override
-    public int read(byte[] bytes) throws IOException {
-        return readSocket(true, bytes, 0, bytes.length);
+    public int read(byte[] bytes, int off, int len) throws IOException {
+        return readSocket(true, bytes, off, len);
     }
 
 
@@ -147,10 +152,11 @@ public class UpgradeNioProcessor extends
     /*
      * Adapted from the NioOutputBuffer
      */
-    private synchronized int writeToSocket(byte[] bytes) throws IOException {
+    private synchronized int writeToSocket(byte[] bytes, int off, int len)
+            throws IOException {
 
         nioChannel.getBufHandler().getWriteBuffer().clear();
-        nioChannel.getBufHandler().getWriteBuffer().put(bytes);
+        nioChannel.getBufHandler().getWriteBuffer().put(bytes, off, len);
         nioChannel.getBufHandler().getWriteBuffer().flip();
 
         int written = 0;

Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeOutbound.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeOutbound.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeOutbound.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeOutbound.java Thu Mar 15 23:17:47 2012
@@ -42,4 +42,9 @@ public class UpgradeOutbound extends Out
     public void write(int b) throws IOException {
         processor.write(b);
     }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        processor.write(b, off, len);
+    }
 }

Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeProcessor.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeProcessor.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/upgrade/UpgradeProcessor.java Thu Mar 15 23:17:47 2012
@@ -43,10 +43,11 @@ public abstract class UpgradeProcessor<S
     // Output methods
     public abstract void flush() throws IOException;
     public abstract void write(int b) throws IOException;
+    public abstract void write(byte[] b, int off, int len) throws IOException;
 
     // Input methods
     public abstract int read() throws IOException;
-    public abstract int read(byte[] bytes) throws IOException;
+    public abstract int read(byte[] bytes, int off, int len) throws IOException;
 
     @Override
     public final UpgradeInbound getUpgradeInbound() {

Modified: tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java (original)
+++ tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java Thu Mar 15 23:17:47 2012
@@ -46,7 +46,7 @@ public class TestWebSocket extends Tomca
 
     private OutputStream os;
     private InputStream is;
-    boolean isContinuation = false;
+    private boolean isContinuation = false;
 
     @Test
     public void testSimple() throws Exception {

Modified: tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoMessage.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoMessage.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoMessage.java (original)
+++ tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoMessage.java Thu Mar 15 23:17:47 2012
@@ -38,12 +38,12 @@ public class EchoMessage extends WebSock
 
         @Override
         protected void onBinaryMessage(ByteBuffer message) throws IOException {
-            getOutbound().writeBinaryMessage(message);
+            getWsOutbound().writeBinaryMessage(message);
         }
 
         @Override
         protected void onTextMessage(CharBuffer message) throws IOException {
-            getOutbound().writeTextMessage(message);
+            getWsOutbound().writeTextMessage(message);
         }
     }
 }

Modified: tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoStream.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoStream.java?rev=1301264&r1=1301263&r2=1301264&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoStream.java (original)
+++ tomcat/tc7.0.x/trunk/webapps/examples/WEB-INF/classes/websocket/EchoStream.java Thu Mar 15 23:17:47 2012
@@ -39,7 +39,7 @@ public class EchoStream extends WebSocke
         @Override
         protected void onBinaryData(InputStream is) throws IOException {
             // Simply echo the data to back to the client.
-            WsOutbound outbound = getOutbound();
+            WsOutbound outbound = getWsOutbound();
 
             int i = is.read();
             while (i != -1) {
@@ -53,7 +53,7 @@ public class EchoStream extends WebSocke
         @Override
         protected void onTextData(Reader r) throws IOException {
             // Simply echo the data to back to the client.
-            WsOutbound outbound = getOutbound();
+            WsOutbound outbound = getWsOutbound();
 
             int c = r.read();
             while (c != -1) {



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org