WebSocket: Implement hybi framing
authoryutak@chromium.org <yutak@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Jul 2011 08:57:29 +0000 (08:57 +0000)
committeryutak@chromium.org <yutak@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Jul 2011 08:57:29 +0000 (08:57 +0000)
https://bugs.webkit.org/show_bug.cgi?id=64522

Reviewed by Kent Tamura.

Source/WebCore:

Implement WebSocket framing protocol which is mainly described in
<http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#section-4> and
<http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#section-6>.

Hybi protocol introduces a new frame format which is drastically different from
the old one. Notable differences are:
- Binary data support.
- Fragmentation support: a single message can be fragmented to multiple frames.
- Ping-pong support.
- Masking: frame content of a client must be masked to prevent cross-protocol attacks.

This patch covers the following features:
- Send a pong frame when a ping frame is received.
- Receive fragmented frames.
- Receive masked frames. (Servers do not have to mask frames, but they may if they wish.)

The following features are NOT implemented yet:
- Send or receive binary messages.
- Send a ping message.
- Send fragmented frames. (It is unclear whether this is necessary.)
- Rewrite the frame content by WebSocket protocol extensions (like frame compression).

New tests: http/tests/websocket/tests/hybi/broken-utf8.html
           http/tests/websocket/tests/hybi/fragmented-control-frame.html
           http/tests/websocket/tests/hybi/fragmented-frames.html
           http/tests/websocket/tests/hybi/interleaved-fragments.html
           http/tests/websocket/tests/hybi/long-control-frame.html
           http/tests/websocket/tests/hybi/masked-frames.html
           http/tests/websocket/tests/hybi/pong.html
           http/tests/websocket/tests/hybi/reserved-bits.html
           http/tests/websocket/tests/hybi/reserved-opcodes.html
           http/tests/websocket/tests/hybi/too-long-payload.html

* websockets/WebSocketChannel.cpp:
(WebCore::WebSocketChannel::WebSocketChannel):
(WebCore::WebSocketChannel::send):
The original content of send() was moved to a private method sendFrameHixie76().
(WebCore::WebSocketChannel::fail):
Stop handling incoming data after the WebSocket connection is failed.
It was unclear to me whether we should do the same thing for hixie-76 connection;
for now, I kept the original behavior.
(WebCore::WebSocketChannel::processBuffer):
(WebCore::WebSocketChannel::resumeTimerFired):
(WebCore::WebSocketChannel::startClosingHandshake):
(WebCore::WebSocketChannel::closingTimerFired):
(WebCore::WebSocketChannel::parseFrame):
(WebCore::WebSocketChannel::processFrame):
(WebCore::WebSocketChannel::processFrameHixie76):
(WebCore::WebSocketChannel::sendFrame):
(WebCore::WebSocketChannel::sendFrameHixie76):
* websockets/WebSocketChannel.h:
(WebCore::WebSocketChannel::isNonControlOpCode):
(WebCore::WebSocketChannel::isControlOpCode):
(WebCore::WebSocketChannel::isReservedOpCode):

LayoutTests:

Fix existing tests so they match the new frame format, and add tests for the new frame types
and error conditions related to the new frame format.

Unskip hybi tests on mac, win and chromium. Other ports (wk2, qt and gtk) still skip these tests
because they do not support changing the value of "WebKitHixie76WebSocketProtocolEnabled"
preference key via layoutTestController.overridePreferences() yet.

* http/tests/websocket/tests/hybi/broken-utf8-expected.txt: Added.
* http/tests/websocket/tests/hybi/broken-utf8.html: Added.
* http/tests/websocket/tests/hybi/broken-utf8_wsh.py: Added.
* http/tests/websocket/tests/hybi/client-close-expected.txt:
* http/tests/websocket/tests/hybi/client-close.html:
The format of a close frame has been changed. Currently, we do not include any payload
in a close frame, thus it must start with "\x88\x80" (see section 4.1 of hybi-10
specification for more details).
* http/tests/websocket/tests/hybi/client-close_wsh.py:
* http/tests/websocket/tests/hybi/fragmented-control-frame-expected.txt: Added.
* http/tests/websocket/tests/hybi/fragmented-control-frame.html: Added.
* http/tests/websocket/tests/hybi/fragmented-control-frame_wsh.py: Added.
* http/tests/websocket/tests/hybi/fragmented-frames-expected.txt: Added.
* http/tests/websocket/tests/hybi/fragmented-frames.html: Added.
* http/tests/websocket/tests/hybi/fragmented-frames_wsh.py: Added.
* http/tests/websocket/tests/hybi/interleaved-fragments-expected.txt: Added.
* http/tests/websocket/tests/hybi/interleaved-fragments.html: Added.
* http/tests/websocket/tests/hybi/interleaved-fragments_wsh.py: Added.
* http/tests/websocket/tests/hybi/long-control-frame-expected.txt: Added.
* http/tests/websocket/tests/hybi/long-control-frame.html: Added.
* http/tests/websocket/tests/hybi/long-control-frame_wsh.py: Added.
* http/tests/websocket/tests/hybi/masked-frames-expected.txt: Added.
* http/tests/websocket/tests/hybi/masked-frames.html: Added.
* http/tests/websocket/tests/hybi/masked-frames_wsh.py: Added.
* http/tests/websocket/tests/hybi/pong-expected.txt: Added.
* http/tests/websocket/tests/hybi/pong.html: Added.
* http/tests/websocket/tests/hybi/pong_wsh.py: Added.
* http/tests/websocket/tests/hybi/reserved-bits-expected.txt: Added.
* http/tests/websocket/tests/hybi/reserved-bits.html: Added.
* http/tests/websocket/tests/hybi/reserved-bits_wsh.py: Added.
* http/tests/websocket/tests/hybi/reserved-opcodes-expected.txt: Added.
* http/tests/websocket/tests/hybi/reserved-opcodes.html: Added.
* http/tests/websocket/tests/hybi/reserved-opcodes_wsh.py: Added.
* http/tests/websocket/tests/hybi/send2_wsh.py:
Send two text frames at once.
* http/tests/websocket/tests/hybi/too-long-payload-expected.txt: Added.
* http/tests/websocket/tests/hybi/too-long-payload.html: Added.
* http/tests/websocket/tests/hybi/too-long-payload_wsh.py: Added.
* platform/chromium/test_expectations.txt:
Derive test expectations of hixie76 tests, because these tests are likely to behave
the same way as hixie76 tests. Will be checked later whether they really do.
* platform/mac/Skipped:
* platform/win/Skipped:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@91243 268f45cc-cd09-0410-ab3c-d52691b4dbfc

41 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/websocket/tests/hybi/broken-utf8-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/broken-utf8.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/broken-utf8_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/client-close-expected.txt
LayoutTests/http/tests/websocket/tests/hybi/client-close.html
LayoutTests/http/tests/websocket/tests/hybi/client-close_wsh.py
LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/long-control-frame-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/long-control-frame.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/long-control-frame_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/masked-frames-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/masked-frames.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/masked-frames_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/pong-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/pong.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/pong_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-bits-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-bits.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-bits_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes_wsh.py [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/send2_wsh.py
LayoutTests/http/tests/websocket/tests/hybi/too-long-payload-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/too-long-payload.html [new file with mode: 0644]
LayoutTests/http/tests/websocket/tests/hybi/too-long-payload_wsh.py [new file with mode: 0644]
LayoutTests/platform/chromium/test_expectations.txt
LayoutTests/platform/mac/Skipped
LayoutTests/platform/win/Skipped
Source/WebCore/ChangeLog
Source/WebCore/websockets/WebSocketChannel.cpp
Source/WebCore/websockets/WebSocketChannel.h

index ffc3f9f6037751d9700634f1902dbf7a56da9793..7c71b8694641b02651eafed4c1eceae5a2a35647 100644 (file)
@@ -1,3 +1,61 @@
+2011-07-19  Yuta Kitamura  <yutak@chromium.org>
+
+        WebSocket: Implement hybi framing
+        https://bugs.webkit.org/show_bug.cgi?id=64522
+
+        Reviewed by Kent Tamura.
+
+        Fix existing tests so they match the new frame format, and add tests for the new frame types
+        and error conditions related to the new frame format.
+
+        Unskip hybi tests on mac, win and chromium. Other ports (wk2, qt and gtk) still skip these tests
+        because they do not support changing the value of "WebKitHixie76WebSocketProtocolEnabled"
+        preference key via layoutTestController.overridePreferences() yet.
+
+        * http/tests/websocket/tests/hybi/broken-utf8-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/broken-utf8.html: Added.
+        * http/tests/websocket/tests/hybi/broken-utf8_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/client-close-expected.txt:
+        * http/tests/websocket/tests/hybi/client-close.html:
+        The format of a close frame has been changed. Currently, we do not include any payload
+        in a close frame, thus it must start with "\x88\x80" (see section 4.1 of hybi-10
+        specification for more details).
+        * http/tests/websocket/tests/hybi/client-close_wsh.py:
+        * http/tests/websocket/tests/hybi/fragmented-control-frame-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/fragmented-control-frame.html: Added.
+        * http/tests/websocket/tests/hybi/fragmented-control-frame_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/fragmented-frames-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/fragmented-frames.html: Added.
+        * http/tests/websocket/tests/hybi/fragmented-frames_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/interleaved-fragments-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/interleaved-fragments.html: Added.
+        * http/tests/websocket/tests/hybi/interleaved-fragments_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/long-control-frame-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/long-control-frame.html: Added.
+        * http/tests/websocket/tests/hybi/long-control-frame_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/masked-frames-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/masked-frames.html: Added.
+        * http/tests/websocket/tests/hybi/masked-frames_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/pong-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/pong.html: Added.
+        * http/tests/websocket/tests/hybi/pong_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/reserved-bits-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/reserved-bits.html: Added.
+        * http/tests/websocket/tests/hybi/reserved-bits_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/reserved-opcodes-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/reserved-opcodes.html: Added.
+        * http/tests/websocket/tests/hybi/reserved-opcodes_wsh.py: Added.
+        * http/tests/websocket/tests/hybi/send2_wsh.py:
+        Send two text frames at once.
+        * http/tests/websocket/tests/hybi/too-long-payload-expected.txt: Added.
+        * http/tests/websocket/tests/hybi/too-long-payload.html: Added.
+        * http/tests/websocket/tests/hybi/too-long-payload_wsh.py: Added.
+        * platform/chromium/test_expectations.txt:
+        Derive test expectations of hixie76 tests, because these tests are likely to behave
+        the same way as hixie76 tests. Will be checked later whether they really do.
+        * platform/mac/Skipped:
+        * platform/win/Skipped:
+
 2011-07-19  Nikolas Zimmermann  <nzimmermann@rim.com>
 
         REGRESSION (r88913): Preview in Safari's snippet editor has a fixed height instead of filling the entire pane
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8-expected.txt
new file mode 100644 (file)
index 0000000..38b96dd
--- /dev/null
@@ -0,0 +1,12 @@
+CONSOLE MESSAGE: line 0: Could not decode a text frame as UTF-8.
+Test whether WebSocket aborts the connection when it receives a text frame containing broken UTF-8 data.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8.html b/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8.html
new file mode 100644 (file)
index 0000000..b644a6a
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket aborts the connection when it receives a text frame containing broken UTF-8 data.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var ws = new WebSocket("ws://127.0.0.1:8880/websocket/tests/hybi/broken-utf8");
+var closeEvent;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    testFailed("onmessage() was called. (message = \"" + message + "\")");
+};
+
+ws.onclose = function(event)
+{
+    debug("onclose() was called.");
+    closeEvent = event;
+    shouldBeFalse("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/broken-utf8_wsh.py
new file mode 100644 (file)
index 0000000..950b574
--- /dev/null
@@ -0,0 +1,11 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    payload = 'This text should be ignored. \xff' # '\xff' will never appear in UTF-8 encoded data.
+    request.connection.write(stream.create_header(common.OPCODE_TEXT, len(payload), 1, 0, 0, 0, 0) + payload)
index 2668fd94232491f7afe8d57d1115203ef4e59093..b120094f19cd813dcf688fe7b687aa263adbd954 100644 (file)
@@ -3,9 +3,9 @@ WebSocket: Test client-initiated close.
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
 Connected
-Received: close_frame='\xff\x00'
+Received: close_frame[:2]='\x88\x80'
 Closed
-PASS receivedMessage is "close_frame='\\xff\\x00'"
+PASS receivedMessage is "close_frame[:2]='\\x88\\x80'"
 PASS closeEvent.wasClean is true
 PASS successfullyParsed is true
 
index bda5143328f23a6f07c5a206c6014c9330709671..ac29e7fc6b54dbc52f9ed158d943ca895f71f736 100644 (file)
@@ -34,7 +34,7 @@ ws.onclose = function(event)
 {
     debug("Closed");
     closeEvent = event;
-    shouldBeEqualToString("receivedMessage", "close_frame='\\xff\\x00'");
+    shouldBeEqualToString("receivedMessage", "close_frame[:2]='\\x88\\x80'");
     shouldBeTrue("closeEvent.wasClean");
     finishJSTest();
 };
index 7ed28bffff0249f4a0d7bbcc3fb832ab879c0820..868e06cb71b69999d90fc0b2a8b88caa0578f0bd 100644 (file)
@@ -7,16 +7,17 @@ def web_socket_do_extra_handshake(request):
 
 def web_socket_transfer_data(request):
     # Wait for a close frame sent from the client.
-    close_frame = request.ws_stream.receive_bytes(2)
+    close_frame = request.ws_stream.receive_bytes(6)
 
-    # Tell the client what we have received.
-    msgutil.send_message(request, 'close_frame=%r' % close_frame)
+    # Send only first two bytes of the received frame. The remaining four bytes are
+    # "masking key", which changes every time the test runs.
+    msgutil.send_message(request, 'close_frame[:2]=%r' % close_frame[:2])
 
     # If the following assertion fails, AssertionError will be raised,
     # which will prevent pywebsocket from sending a close frame.
     # In this case, the client will fail to finish closing handshake, thus
     # closeEvent.wasClean will become false.
-    assert close_frame == '\xff\x00'
+    assert close_frame[:2] == '\x88\x80'
 
     # Pretend we have received a close frame from the client.
     # After this function exits, pywebsocket will send a close frame automatically.
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame-expected.txt
new file mode 100644 (file)
index 0000000..24c6b99
--- /dev/null
@@ -0,0 +1,11 @@
+CONSOLE MESSAGE: line 0: Received fragmented control frame: opcode = 9
+Test whether WebSocket rejects a fragmented control frame and aborts the connection.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame.html b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame.html
new file mode 100644 (file)
index 0000000..006d913
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket rejects a fragmented control frame and aborts the connection.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/fragmented-control-frame";
+var ws = new WebSocket(url);
+var closeEvent;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    testFailed("onmessage() was called. (message = \"" + message + "\")");
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeFalse("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-control-frame_wsh.py
new file mode 100644 (file)
index 0000000..a016e77
--- /dev/null
@@ -0,0 +1,12 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import msgutil
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    # Fragmented control frame is prohibited. The client must abort the connection.
+    request.connection.write(stream.create_text_frame('This message should be ignored.', opcode=common.OPCODE_PING, fin=0))
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames-expected.txt
new file mode 100644 (file)
index 0000000..3c67d9a
--- /dev/null
@@ -0,0 +1,17 @@
+Receive fragmented WebSocket frames.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+onmessage() was called. (message = "First message")
+onmessage() was called. (message = "Second message")
+onmessage() was called. (message = "Third message")
+PASS closeEvent.wasClean is true
+PASS actualMessages.length === expectedMessages.length is true
+PASS actualMessages[0] is "First message"
+PASS actualMessages[1] is "Second message"
+PASS actualMessages[2] is "Third message"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames.html b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames.html
new file mode 100644 (file)
index 0000000..b080787
--- /dev/null
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Receive fragmented WebSocket frames.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/fragmented-frames";
+var ws = new WebSocket(url);
+var closeEvent;
+var expectedMessages = ["First message", "Second message", "Third message"];
+var actualMessages = [];
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+    ws.close();
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    debug("onmessage() was called. (message = \"" + message + "\")");
+    actualMessages.push(message);
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeTrue("closeEvent.wasClean");
+    shouldBeTrue("actualMessages.length === expectedMessages.length");
+    for (var i = 0; i < expectedMessages.length; ++i)
+        shouldBeEqualToString("actualMessages[" + i + "]", expectedMessages[i]);
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/fragmented-frames_wsh.py
new file mode 100644 (file)
index 0000000..b6f3e58
--- /dev/null
@@ -0,0 +1,34 @@
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    # send_message's third argument corresponds to "fin" bit;
+    # it is set to True if this frame is the final fragment of a message.
+    msgutil.send_message(request, 'First ', False)
+    msgutil.send_message(request, 'message', True)
+
+    # Empty fragment is allowed.
+    msgutil.send_message(request, '', False)
+    msgutil.send_message(request, 'Second ', False)
+    msgutil.send_message(request, '', False)
+    msgutil.send_message(request, 'message', False)
+    msgutil.send_message(request, '', True)
+
+    # Fragment aggressively.
+    msgutil.send_message(request, 'T', False)
+    msgutil.send_message(request, 'h', False)
+    msgutil.send_message(request, 'i', False)
+    msgutil.send_message(request, 'r', False)
+    msgutil.send_message(request, 'd', False)
+    msgutil.send_message(request, ' ', False)
+    msgutil.send_message(request, 'm', False)
+    msgutil.send_message(request, 'e', False)
+    msgutil.send_message(request, 's', False)
+    msgutil.send_message(request, 's', False)
+    msgutil.send_message(request, 'a', False)
+    msgutil.send_message(request, 'g', False)
+    msgutil.send_message(request, 'e', True)
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments-expected.txt
new file mode 100644 (file)
index 0000000..6bc2813
--- /dev/null
@@ -0,0 +1,11 @@
+CONSOLE MESSAGE: line 0: Received new data frame but previous continuous frame is unfinished.
+Test whether WebSocket rejects interleaved fragmented frames.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments.html b/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments.html
new file mode 100644 (file)
index 0000000..1a3208b
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket rejects interleaved fragmented frames.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/interleaved-fragments";
+var ws = new WebSocket(url);
+var closeEvent;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    testFailed("onmessage() was called. (message = \"" + message + "\")");
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeFalse("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/interleaved-fragments_wsh.py
new file mode 100644 (file)
index 0000000..a5daeca
--- /dev/null
@@ -0,0 +1,12 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    # A new frame is arrived before the previous fragmented frame has finished.
+    request.connection.write(stream.create_text_frame('This message ', opcode=common.OPCODE_TEXT, fin=0))
+    request.connection.write(stream.create_text_frame('should be ignored.', opcode=common.OPCODE_TEXT, fin=1)) # Not OPCODE_CONTINUATION.
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame-expected.txt
new file mode 100644 (file)
index 0000000..870395d
--- /dev/null
@@ -0,0 +1,11 @@
+CONSOLE MESSAGE: line 0: Received control frame having too long payload: 126 bytes
+Test whether WebSocket rejects control frames longer than 125 bytes.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame.html b/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame.html
new file mode 100644 (file)
index 0000000..83e1848
--- /dev/null
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket rejects control frames longer than 125 bytes.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/long-control-frame";
+var ws = new WebSocket(url);
+var closeEvent;
+var message;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeFalse("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/long-control-frame_wsh.py
new file mode 100644 (file)
index 0000000..41f48a3
--- /dev/null
@@ -0,0 +1,12 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    # All control frames must have a payload length of 125 bytes or less.
+    message = 'X' * 126
+    request.connection.write(stream.create_text_frame(message, opcode=common.OPCODE_PING, fin=1))
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/masked-frames-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/masked-frames-expected.txt
new file mode 100644 (file)
index 0000000..8f7b017
--- /dev/null
@@ -0,0 +1,18 @@
+Receive masked WebSocket frames.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+onmessage() was called. (message = "First message")
+onmessage() was called. (message = "Fragmented message")
+onmessage() was called. (message = "")
+onmessage() was called. (message = "END")
+PASS actualMessages.length === expectedMessages.length is true
+PASS actualMessages[0] is "First message"
+PASS actualMessages[1] is "Fragmented message"
+PASS actualMessages[2] is ""
+PASS closeEvent.wasClean is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/masked-frames.html b/LayoutTests/http/tests/websocket/tests/hybi/masked-frames.html
new file mode 100644 (file)
index 0000000..bf0c924
--- /dev/null
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Receive masked WebSocket frames.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/masked-frames";
+var ws = new WebSocket(url);
+var closeEvent;
+var expectedMessages = ["First message", "Fragmented message", ""];
+var actualMessages = [];
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+    ws.close();
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    debug("onmessage() was called. (message = \"" + message + "\")");
+    if (message === "END") {
+        ws.close();
+        return;
+    }
+    actualMessages.push(message);
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeTrue("actualMessages.length === expectedMessages.length");
+    for (var i = 0; i < expectedMessages.length; ++i)
+        shouldBeEqualToString("actualMessages[" + i + "]", expectedMessages[i]);
+    shouldBeTrue("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/masked-frames_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/masked-frames_wsh.py
new file mode 100644 (file)
index 0000000..209967a
--- /dev/null
@@ -0,0 +1,34 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    # pywebsocket does not mask message by default. We need to build a frame manually to mask it.
+    request.connection.write(stream.create_text_frame('First message', mask=True))
+
+    request.connection.write(stream.create_text_frame('Fragmented ', opcode=common.OPCODE_TEXT, fin=0, mask=True))
+    request.connection.write(stream.create_text_frame('message', opcode=common.OPCODE_CONTINUATION, fin=1, mask=True))
+
+    request.connection.write(stream.create_text_frame('', mask=True))
+
+    msgutil.send_message(request, 'END')
+
+    # Wait for the client to start closing handshake.
+    # To receive a close frame, we must use an internal method of request.ws_stream.
+    opcode, payload, final, reserved1, reserved2, reserved3 = request.ws_stream._receive_frame()
+    assert opcode == common.OPCODE_CLOSE
+    assert final
+    assert not reserved1
+    assert not reserved2
+    assert not reserved3
+
+    # Send a masked close frame. Clients should be able to handle this frame and
+    # the WebSocket object should be closed cleanly.
+    request.connection.write(stream.create_close_frame('', mask=True))
+
+    raise Exception('Abort the connection') # Prevents pywebsocket from starting its own closing handshake.
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/pong-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/pong-expected.txt
new file mode 100644 (file)
index 0000000..774fd1e
--- /dev/null
@@ -0,0 +1,12 @@
+Test whether WebSocket correctly responds to a ping message sent from the server.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+onmessage() was called. (message = "PASS")
+PASS message is "PASS"
+PASS closeEvent.wasClean is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/pong.html b/LayoutTests/http/tests/websocket/tests/hybi/pong.html
new file mode 100644 (file)
index 0000000..65ad040
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket correctly responds to a ping message sent from the server.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var url = "ws://localhost:8880/websocket/tests/hybi/pong";
+var ws = new WebSocket(url);
+var closeEvent;
+var message;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onmessage = function(event)
+{
+    message = event.data;
+    debug("onmessage() was called. (message = \"" + message + "\")");
+    shouldBeEqualToString("message", "PASS");
+};
+
+ws.onclose = function(event)
+{
+    closeEvent = event;
+    shouldBeTrue("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/pong_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/pong_wsh.py
new file mode 100644 (file)
index 0000000..612aac5
--- /dev/null
@@ -0,0 +1,19 @@
+from mod_pywebsocket import common
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    msgutil.send_ping(request, 'Hello, world!')
+
+    # We need to use an internal function to detect a pong frame from the client.
+    opcode, payload, final, reserved1, reserved2, reserved3 = request.ws_stream._receive_frame()
+    if opcode == common.OPCODE_PONG and payload == 'Hello, world!' and final and not reserved1 and not reserved2 and not reserved3:
+        msgutil.send_message(request, 'PASS')
+    else:
+        msgutil.send_message(request,
+                             'FAIL: Received unexpected frame: opcode = %r, payload = %r, final = %r, reserved1 = %r, reserved2 = %r, reserved3 = %r' %
+                             (opcode, payload, final, reserved1, reserved2, reserved3))
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits-expected.txt
new file mode 100644 (file)
index 0000000..030726a
--- /dev/null
@@ -0,0 +1,23 @@
+CONSOLE MESSAGE: line 0: One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0
+CONSOLE MESSAGE: line 0: One or more reserved bits are on: reserved1 = 0, reserved2 = 1, reserved3 = 0
+CONSOLE MESSAGE: line 0: One or more reserved bits are on: reserved1 = 0, reserved2 = 0, reserved3 = 1
+Test whether WebSocket rejects frames whose reserved bit is on.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+Bit 1: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Bit 2: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Bit 3: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits.html b/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits.html
new file mode 100644 (file)
index 0000000..3b6621f
--- /dev/null
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+description("Test whether WebSocket rejects frames whose reserved bit is on.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var closeEvent;
+
+// bitNumber must be 1, 2, or 3.
+function doTest(bitNumber)
+{
+    var url = "ws://localhost:8880/websocket/tests/hybi/reserved-bits?bit=" + bitNumber;
+    var ws = new WebSocket(url);
+
+    debug("Bit " + bitNumber + ": Test started.");
+
+    ws.onopen = function()
+    {
+        debug("onopen() was called.");
+    };
+
+    ws.onmessage = function(event)
+    {
+        var message = event.data;
+        testFailed("onmessage() was called. (message = \"" + message + "\")");
+    };
+
+    ws.onclose = function(event)
+    {
+        debug("onclose() was called.");
+        closeEvent = event;
+        shouldBeFalse("closeEvent.wasClean");
+        if (bitNumber === 3)
+            finishJSTest();
+        else
+            doTest(bitNumber + 1);
+    };
+}
+
+doTest(1);
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/reserved-bits_wsh.py
new file mode 100644 (file)
index 0000000..4ad0dfd
--- /dev/null
@@ -0,0 +1,26 @@
+import re
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    match = re.search(r'\?bit=(\d+)$', request.ws_resource)
+    if match is None:
+        msgutil.send_message(request, 'FAIL: Query value is incorrect or missing')
+        return
+
+    bit = int(match.group(1))
+    message = "This message should be ignored."
+    if bit == 1:
+        frame = stream.create_header(common.OPCODE_TEXT, len(message), 1, 1, 0, 0, 0) + message
+    elif bit == 2:
+        frame = stream.create_header(common.OPCODE_TEXT, len(message), 1, 0, 1, 0, 0) + message
+    elif bit == 3:
+        frame = stream.create_header(common.OPCODE_TEXT, len(message), 1, 0, 0, 1, 0) + message
+    else:
+        frame = stream.create_text_frame('FAIL: Invalid bit number: %d' % bit)
+    request.connection.write(frame)
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes-expected.txt
new file mode 100644 (file)
index 0000000..15590c6
--- /dev/null
@@ -0,0 +1,58 @@
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 3
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 4
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 5
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 6
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 7
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 11
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 12
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 13
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 14
+CONSOLE MESSAGE: line 0: Unrecognized frame opcode: 15
+Tests whether WebSocket ignores frames with reserved opcode.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+Opcode 3: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 4: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 5: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 6: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 7: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 11: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 12: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 13: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 14: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+Opcode 15: Test started.
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes.html b/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes.html
new file mode 100644 (file)
index 0000000..27195c6
--- /dev/null
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script type="text/javascript">
+description("Tests whether WebSocket ignores frames with reserved opcode.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var closeEvent;
+
+function doTest(opcode)
+{
+    if (opcode >= 0x10) {
+        finishJSTest();
+        return;
+    }
+    if (opcode == 0x0 || opcode == 0x1 || opcode == 0x2 || opcode == 0x8 || opcode == 0x9 || opcode == 0xA) {
+        doTest(opcode + 1);
+        return;
+    }
+
+    debug("Opcode " + opcode + ": Test started.");
+
+    var url = "ws://localhost:8880/websocket/tests/hybi/reserved-opcodes?opcode=" + opcode;
+    var ws = new WebSocket(url);
+
+    ws.onopen = function()
+    {
+        debug("onopen() was called.");
+    };
+
+    ws.onmessage = function(event)
+    {
+        var message = event.data;
+        testFailed("onmessage() was called. (message = \"" + message + "\")");
+    };
+
+    ws.onclose = function(event)
+    {
+        debug("onclose() was called.");
+        closeEvent = event;
+        shouldBeFalse("closeEvent.wasClean");
+        doTest(opcode + 1);
+    };
+}
+
+doTest(0);
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/reserved-opcodes_wsh.py
new file mode 100644 (file)
index 0000000..96495c8
--- /dev/null
@@ -0,0 +1,19 @@
+import re
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    match = re.search(r'\?opcode=(\d+)$', request.ws_resource)
+    if match is None:
+        msgutil.send_message(request, 'FAIL: Query value is incorrect or missing')
+        return
+
+    opcode = int(match.group(1))
+    payload = 'This text should be ignored. (opcode = %d)' % opcode
+    request.connection.write(stream.create_header(opcode, len(payload), 1, 0, 0, 0, 0) + payload)
index e2217fa3d52c0fe3d98d62c4d18142709dacf172..53b6d810a037baca50c76fcecef079155c24ae08 100644 (file)
@@ -1,8 +1,10 @@
+from mod_pywebsocket import stream
+
+
 def web_socket_do_extra_handshake(request):
     pass # Always accept.
 
 
 def web_socket_transfer_data(request):
     # send 2 messages in one packet.
-    request.connection.write("\x00" + "first message" + "\xff" +
-                             "\x00" + "second message" + "\xff")
+    request.connection.write(stream.create_text_frame("first message") + stream.create_text_frame("second message"))
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload-expected.txt b/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload-expected.txt
new file mode 100644 (file)
index 0000000..901b6dc
--- /dev/null
@@ -0,0 +1,12 @@
+CONSOLE MESSAGE: line 0: WebSocket frame length too large: 9223372036854775808 bytes
+Tests whether WebSocket correctly aborts the connection when it receives a frame with too long payload.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+onopen() was called.
+onclose() was called.
+PASS closeEvent.wasClean is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload.html b/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload.html
new file mode 100644 (file)
index 0000000..704be3a
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<link rel="stylesheet" href="../../../../js-test-resources/js-test-style.css">
+<script src="../../../../js-test-resources/js-test-pre.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script type="text/javascript">
+description("Tests whether WebSocket correctly aborts the connection when it receives a frame with too long payload.");
+
+window.jsTestIsAsync = true;
+if (window.layoutTestController)
+    layoutTestController.overridePreference("WebKitHixie76WebSocketProtocolEnabled", 0);
+
+var ws = new WebSocket("ws://127.0.0.1:8880/websocket/tests/hybi/too-long-payload");
+var closeEvent;
+
+ws.onopen = function()
+{
+    debug("onopen() was called.");
+};
+
+ws.onmessage = function(event)
+{
+    var message = event.data;
+    testFailed("onmessage() was called. (message = \"" + message + "\")");
+};
+
+ws.onclose = function(event)
+{
+    debug("onclose() was called.");
+    closeEvent = event;
+    shouldBeFalse("closeEvent.wasClean");
+    finishJSTest();
+};
+
+var successfullyParsed = true;
+</script>
+<script src="../../../../js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload_wsh.py b/LayoutTests/http/tests/websocket/tests/hybi/too-long-payload_wsh.py
new file mode 100644 (file)
index 0000000..5ecda34
--- /dev/null
@@ -0,0 +1,24 @@
+import struct
+import time
+from mod_pywebsocket import common
+
+
+def web_socket_do_extra_handshake(request):
+    pass
+
+
+def web_socket_transfer_data(request):
+    length = 0x8000000000000000
+
+    # pywebsocket refuses to send a frame with too long payload.
+    # Thus, we need to build a frame manually.
+    header = chr(0x80 | common.OPCODE_TEXT) # 0x80 is for "fin" bit.
+    header += chr(127)
+    header += struct.pack('!Q', length)
+    request.connection.write(header)
+
+    # Send data indefinitely to simulate a real (broken) server sending a big frame.
+    # A client should ignore these bytes and abort the connection.
+    while True:
+        request.connection.write('X' * 4096)
+        time.sleep(1)
index 002449629db79e369e0e05d8a14a534ca2c58191..bdfc05215bd26e7b6ad8b7fdb69b8cfbe97f107b 100644 (file)
@@ -46,6 +46,7 @@ BUGCR18702 WIN : http/tests/xmlhttprequest/supported-xml-content-types.html = PA
 BUGCR36539 SLOW : http/tests/misc/uncacheable-script-repeated.html = PASS
 
 BUGCR32018 SLOW : http/tests/websocket/tests/hixie76/frame-lengths.html = PASS
+BUGCR32018 SLOW : http/tests/websocket/tests/hybi/frame-lengths.html = PASS
 BUGCR69513 DEBUG SLOW : html5lib/webkit-resumer.html = PASS
 
 BUGWK55913 RELEASE SLOW : jquery/effects.html = PASS
@@ -105,6 +106,7 @@ WONTFIX SKIP : http/tests/workers = PASS TIMEOUT FAIL
 WONTFIX SKIP : http/tests/xmlhttprequest/workers = PASS TIMEOUT FAIL
 WONTFIX SKIP : http/tests/eventsource/workers = PASS TIMEOUT FAIL
 WONTFIX SKIP : http/tests/websocket/tests/hixie76/workers/ = PASS TIMEOUT FAIL
+WONTFIX SKIP : http/tests/websocket/tests/hybi/workers/ = PASS TIMEOUT FAIL
 WONTFIX SKIP : inspector/debugger/script-formatter.html = PASS TIMEOUT FAIL
 
 // Page Cache - based tests. Chromium disables page cache because the WebKit page cache keeps previously
@@ -156,9 +158,6 @@ WONTFIX SKIP : animations/animation-api-1.html = FAIL
 BUGWK60877 SKIP : loader/navigation-while-deferring-loads.html = FAIL
 BUGWK60877 SKIP : loader/load-defer-resume-crash.html = FAIL
 
-// Skipped until new WebSocket protocol is implemented.
-BUGWK50099 SKIP : http/tests/websocket/tests/hybi/ = PASS FAIL TIMEOUT
-
 // CSS3 Selectors3 test suite
 BUGCR89468 : css3/selectors3 = PASS FAIL
 
@@ -1721,6 +1720,7 @@ BUGCR31623 WIN : http/tests/appcache/remove-cache.html = PASS TEXT TIMEOUT
 
 // WebKit roll 52852 -> 52867
 BUGCR32018 DEBUG : http/tests/websocket/tests/hixie76/simple-stress.html = PASS TEXT TIMEOUT
+BUGCR32018 DEBUG : http/tests/websocket/tests/hybi/simple-stress.html = PASS TEXT TIMEOUT
 
 // V8's implementation of getOwnPropertyNames has different results for built-in
 // functions.
@@ -2800,6 +2800,7 @@ BUGCR73080 LINUX DEBUG : fast/forms/form-attribute-elements-order.html = PASS TI
 BUGCR73091 : animations/play-state-suspend.html = PASS TEXT
 BUGCR73092 WIN : fast/dom/gc-11.html = PASS TEXT
 BUGCR73094 LINUX : http/tests/websocket/tests/hixie76/send-after-close-on-unload.html = PASS TIMEOUT
+BUGCR73094 LINUX : http/tests/websocket/tests/hybi/send-after-close-on-unload.html = PASS TIMEOUT
 
 BUGV8_1168 : fast/js/mozilla/eval/exhaustive-fun-normalcaller-indirect-strictcode.html = TEXT
 BUGV8_1168 : fast/js/mozilla/eval/exhaustive-fun-strictcaller-indirect-strictcode.html = TEXT
@@ -3645,6 +3646,7 @@ BUGWK61664 LINUX : svg/zoom/page/relative-sized-document-scrollbars.svg = IMAGE
 BUGCR84317 DEBUG : svg/wicd/test-scalable-background-image1.xhtml = CRASH
 
 BUGWK61767 WIN DEBUG : http/tests/websocket/tests/hixie76/client-close.html = PASS TEXT
+BUGWK61767 WIN DEBUG : http/tests/websocket/tests/hybi/client-close.html = PASS TEXT
 
 BUGWK59782 WIN LINUX DEBUG : svg/dynamic-updates/SVGFEDropShadowElement-dom-dx-attr.html = TIMEOUT
 BUGWK59782 WIN LINUX DEBUG : svg/dynamic-updates/SVGFEDropShadowElement-dom-dy-attr.html = TIMEOUT
index 8825d11dbdba1398c1b2b1519a45e339c6c6f289..c49666c8f759c102ec077337b08e842453492174 100644 (file)
@@ -386,6 +386,3 @@ compositing/rtl/rtl-iframe-fixed-overflow-scrolled.html
 compositing/rtl/rtl-iframe-fixed-overflow.html
 compositing/rtl/rtl-iframe-fixed.html
 compositing/rtl/rtl-iframe-relative.html
-
-# Skipped until new WebSocket protocol is implemented. http://webkit.org/b/50099
-http/tests/websocket/tests/hybi/
index 581e3a5d26722b683199a649f3783750bbdb4e55..84c7ccf1078fc9169f79bc288a02d367e54454e7 100644 (file)
@@ -1346,8 +1346,5 @@ fast/workers/worker-crash-with-invalid-location.html
 # Need to implement getFormValue().
 plugins/form-value.html
 
-# Skipped until new WebSocket protocol is implemented. http://webkit.org/b/50099
-http/tests/websocket/tests/hybi/
-
 # Needs generated results
 css3/selectors3
index e125785fabfd5e32ffd8edcb1e0eed871f6c89e9..9f19c06fde2f73255c3628d4baf0d4fff14ceea6 100644 (file)
@@ -1,3 +1,65 @@
+2011-07-19  Yuta Kitamura  <yutak@chromium.org>
+
+        WebSocket: Implement hybi framing
+        https://bugs.webkit.org/show_bug.cgi?id=64522
+
+        Reviewed by Kent Tamura.
+
+        Implement WebSocket framing protocol which is mainly described in
+        <http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#section-4> and
+        <http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#section-6>.
+
+        Hybi protocol introduces a new frame format which is drastically different from
+        the old one. Notable differences are:
+        - Binary data support.
+        - Fragmentation support: a single message can be fragmented to multiple frames.
+        - Ping-pong support.
+        - Masking: frame content of a client must be masked to prevent cross-protocol attacks.
+
+        This patch covers the following features:
+        - Send a pong frame when a ping frame is received.
+        - Receive fragmented frames.
+        - Receive masked frames. (Servers do not have to mask frames, but they may if they wish.)
+
+        The following features are NOT implemented yet:
+        - Send or receive binary messages.
+        - Send a ping message.
+        - Send fragmented frames. (It is unclear whether this is necessary.)
+        - Rewrite the frame content by WebSocket protocol extensions (like frame compression).
+
+        New tests: http/tests/websocket/tests/hybi/broken-utf8.html
+                   http/tests/websocket/tests/hybi/fragmented-control-frame.html
+                   http/tests/websocket/tests/hybi/fragmented-frames.html
+                   http/tests/websocket/tests/hybi/interleaved-fragments.html
+                   http/tests/websocket/tests/hybi/long-control-frame.html
+                   http/tests/websocket/tests/hybi/masked-frames.html
+                   http/tests/websocket/tests/hybi/pong.html
+                   http/tests/websocket/tests/hybi/reserved-bits.html
+                   http/tests/websocket/tests/hybi/reserved-opcodes.html
+                   http/tests/websocket/tests/hybi/too-long-payload.html
+
+        * websockets/WebSocketChannel.cpp:
+        (WebCore::WebSocketChannel::WebSocketChannel):
+        (WebCore::WebSocketChannel::send):
+        The original content of send() was moved to a private method sendFrameHixie76().
+        (WebCore::WebSocketChannel::fail):
+        Stop handling incoming data after the WebSocket connection is failed.
+        It was unclear to me whether we should do the same thing for hixie-76 connection;
+        for now, I kept the original behavior.
+        (WebCore::WebSocketChannel::processBuffer):
+        (WebCore::WebSocketChannel::resumeTimerFired):
+        (WebCore::WebSocketChannel::startClosingHandshake):
+        (WebCore::WebSocketChannel::closingTimerFired):
+        (WebCore::WebSocketChannel::parseFrame):
+        (WebCore::WebSocketChannel::processFrame):
+        (WebCore::WebSocketChannel::processFrameHixie76):
+        (WebCore::WebSocketChannel::sendFrame):
+        (WebCore::WebSocketChannel::sendFrameHixie76):
+        * websockets/WebSocketChannel.h:
+        (WebCore::WebSocketChannel::isNonControlOpCode):
+        (WebCore::WebSocketChannel::isControlOpCode):
+        (WebCore::WebSocketChannel::isReservedOpCode):
+
 2011-07-19  Nikolas Zimmermann  <nzimmermann@rim.com>
 
         REGRESSION (r88913): Preview in Safari's snippet editor has a fixed height instead of filling the entire pane
index 6fb31af03afcc1cea014eea7a38c11fec7f36a29..e56114f3f4630a0245e60fdd4e21cfc8d8d7ca56 100644 (file)
 #include "WebSocketChannelClient.h"
 #include "WebSocketHandshake.h"
 
-#include <wtf/text/CString.h>
-#include <wtf/text/WTFString.h>
-#include <wtf/text/StringHash.h>
+#include <wtf/CryptographicallyRandomNumber.h>
 #include <wtf/Deque.h>
 #include <wtf/FastMalloc.h>
 #include <wtf/HashMap.h>
+#include <wtf/text/CString.h>
+#include <wtf/text/StringHash.h>
+#include <wtf/text/WTFString.h>
+
+using namespace std;
 
 namespace WebCore {
 
 const double TCPMaximumSegmentLifetime = 2 * 60.0;
 
+// Constants for hybi-10 frame format.
+const unsigned char finalBit = 0x80;
+const unsigned char reserved1Bit = 0x40;
+const unsigned char reserved2Bit = 0x20;
+const unsigned char reserved3Bit = 0x10;
+const unsigned char opCodeMask = 0xF;
+const unsigned char maskBit = 0x80;
+const unsigned char payloadLengthMask = 0x7F;
+const size_t maxPayloadLengthWithoutExtendedLengthField = 125;
+const size_t payloadLengthWithTwoByteExtendedLengthField = 126;
+const size_t payloadLengthWithEightByteExtendedLengthField = 127;
+const size_t maskingKeyWidthInBytes = 4;
+
+const WebSocketChannel::OpCode WebSocketChannel::OpCodeContinuation = 0x0;
+const WebSocketChannel::OpCode WebSocketChannel::OpCodeText = 0x1;
+const WebSocketChannel::OpCode WebSocketChannel::OpCodeBinary = 0x2;
+const WebSocketChannel::OpCode WebSocketChannel::OpCodeClose = 0x8;
+const WebSocketChannel::OpCode WebSocketChannel::OpCodePing = 0x9;
+const WebSocketChannel::OpCode WebSocketChannel::OpCodePong = 0xA;
+
 WebSocketChannel::WebSocketChannel(ScriptExecutionContext* context, WebSocketChannelClient* client, const KURL& url, const String& protocol)
     : m_context(context)
     , m_client(client)
@@ -74,6 +97,7 @@ WebSocketChannel::WebSocketChannel(ScriptExecutionContext* context, WebSocketCha
     , m_unhandledBufferedAmount(0)
     , m_identifier(0)
     , m_useHixie76Protocol(true)
+    , m_hasContinuousFrame(false)
 {
     ASSERT(m_context->isDocument());
     Document* document = static_cast<Document*>(m_context);
@@ -102,17 +126,13 @@ void WebSocketChannel::connect()
     m_handle = SocketStreamHandle::create(m_handshake->url(), this);
 }
 
-bool WebSocketChannel::send(const String& msg)
+bool WebSocketChannel::send(const String& message)
 {
-    LOG(Network, "WebSocketChannel %p send %s", this, msg.utf8().data());
-    ASSERT(m_handle);
-    ASSERT(!m_suspended);
-    Vector<char> buf;
-    buf.append('\0');  // frame type
-    CString utf8 = msg.utf8();
-    buf.append(utf8.data(), utf8.length());
-    buf.append('\xff');  // frame end
-    return m_handle->send(buf.data(), buf.size());
+    LOG(Network, "WebSocketChannel %p send %s", this, message.utf8().data());
+    CString utf8 = message.utf8();
+    if (m_useHixie76Protocol)
+        return sendFrameHixie76(utf8.data(), utf8.length());
+    return sendFrame(OpCodeText, utf8.data(), utf8.length());
 }
 
 unsigned long WebSocketChannel::bufferedAmount() const
@@ -140,6 +160,16 @@ void WebSocketChannel::fail(const String& reason)
     ASSERT(!m_suspended);
     if (m_context)
         m_context->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, reason, 0, m_handshake->clientOrigin(), 0);
+    if (!m_useHixie76Protocol) {
+        // Hybi-10 specification explicitly states we must not continue to handle incoming data
+        // once the WebSocket connection is failed (section 7.1.7).
+        // FIXME: Should we do this in hixie-76 too?
+        m_shouldDiscardReceivedData = true;
+        if (m_buffer)
+            skipBuffer(m_bufferSize); // Save memory.
+        m_hasContinuousFrame = false;
+        m_continuousFrameData.clear();
+    }
     if (m_handle && !m_closed)
         m_handle->disconnect(); // Will call didClose().
 }
@@ -345,6 +375,257 @@ bool WebSocketChannel::processBuffer()
     if (m_handshake->mode() != WebSocketHandshake::Connected)
         return false;
 
+    if (m_useHixie76Protocol)
+        return processFrameHixie76();
+
+    return processFrame();
+}
+
+void WebSocketChannel::resumeTimerFired(Timer<WebSocketChannel>* timer)
+{
+    ASSERT_UNUSED(timer, timer == &m_resumeTimer);
+
+    RefPtr<WebSocketChannel> protect(this); // The client can close the channel, potentially removing the last reference.
+    while (!m_suspended && m_client && m_buffer)
+        if (!processBuffer())
+            break;
+    if (!m_suspended && m_client && m_closed && m_handle)
+        didClose(m_handle.get());
+}
+
+void WebSocketChannel::startClosingHandshake()
+{
+    LOG(Network, "WebSocketChannel %p closing %d %d", this, m_closing, m_receivedClosingHandshake);
+    if (m_closing)
+        return;
+    ASSERT(m_handle);
+    bool sentSuccessfully;
+    if (m_useHixie76Protocol) {
+        Vector<char> buf;
+        buf.append('\xff');
+        buf.append('\0');
+        sentSuccessfully = m_handle->send(buf.data(), buf.size());
+    } else
+        sentSuccessfully = sendFrame(OpCodeClose, "", 0); // FIXME: Send status code and reason message.
+
+    if (!sentSuccessfully) {
+        m_handle->disconnect();
+        return;
+    }
+    m_closing = true;
+    if (m_client)
+        m_client->didStartClosingHandshake();
+}
+
+void WebSocketChannel::closingTimerFired(Timer<WebSocketChannel>* timer)
+{
+    LOG(Network, "WebSocketChannel %p closing timer", this);
+    ASSERT_UNUSED(timer, &m_closingTimer == timer);
+    if (m_handle)
+        m_handle->disconnect();
+}
+
+WebSocketChannel::ParseFrameResult WebSocketChannel::parseFrame(FrameData& frame)
+{
+    const char* p = m_buffer;
+    const char* bufferEnd = m_buffer + m_bufferSize;
+
+    if (m_bufferSize < 2)
+        return FrameIncomplete;
+
+    unsigned char firstByte = *p++;
+    unsigned char secondByte = *p++;
+
+    bool final = firstByte & finalBit;
+    bool reserved1 = firstByte & reserved1Bit;
+    bool reserved2 = firstByte & reserved2Bit;
+    bool reserved3 = firstByte & reserved3Bit;
+    OpCode opCode = firstByte & opCodeMask;
+
+    bool masked = secondByte & maskBit;
+    uint64_t payloadLength64 = secondByte & payloadLengthMask;
+    if (payloadLength64 > maxPayloadLengthWithoutExtendedLengthField) {
+        int extendedPayloadLengthSize;
+        if (payloadLength64 == payloadLengthWithTwoByteExtendedLengthField)
+            extendedPayloadLengthSize = 2;
+        else {
+            ASSERT(payloadLength64 == payloadLengthWithEightByteExtendedLengthField);
+            extendedPayloadLengthSize = 8;
+        }
+        if (bufferEnd - p < extendedPayloadLengthSize)
+            return FrameIncomplete;
+        payloadLength64 = 0;
+        for (int i = 0; i < extendedPayloadLengthSize; ++i) {
+            payloadLength64 <<= 8;
+            payloadLength64 |= static_cast<unsigned char>(*p++);
+        }
+    }
+
+    // FIXME: UINT64_C(0x7FFFFFFFFFFFFFFF) should be used but it did not compile on Qt bots.
+#if COMPILER(MSVC)
+    static const uint64_t maxPayloadLength = 0x7FFFFFFFFFFFFFFFui64;
+#else
+    static const uint64_t maxPayloadLength = 0x7FFFFFFFFFFFFFFFull;
+#endif
+    size_t maskingKeyLength = masked ? maskingKeyWidthInBytes : 0;
+    if (payloadLength64 > maxPayloadLength || payloadLength64 + maskingKeyLength > numeric_limits<size_t>::max()) {
+        fail("WebSocket frame length too large: " + String::number(payloadLength64) + " bytes");
+        return FrameError;
+    }
+    size_t payloadLength = static_cast<size_t>(payloadLength64);
+
+    if (static_cast<size_t>(bufferEnd - p) < maskingKeyLength + payloadLength)
+        return FrameIncomplete;
+
+    if (masked) {
+        const char* maskingKey = p;
+        char* payload = const_cast<char*>(p + maskingKeyWidthInBytes);
+        for (size_t i = 0; i < payloadLength; ++i)
+            payload[i] ^= maskingKey[i % maskingKeyWidthInBytes]; // Unmask the payload.
+    }
+
+    frame.opCode = opCode;
+    frame.final = final;
+    frame.reserved1 = reserved1;
+    frame.reserved2 = reserved2;
+    frame.reserved3 = reserved3;
+    frame.masked = masked;
+    frame.payload = p + maskingKeyLength;
+    frame.payloadLength = payloadLength;
+    frame.frameEnd = p + maskingKeyLength + payloadLength;
+    return FrameOK;
+}
+
+bool WebSocketChannel::processFrame()
+{
+    ASSERT(m_buffer);
+
+    FrameData frame;
+    if (parseFrame(frame) != FrameOK)
+        return false;
+
+    // Validate the frame data.
+    if (isReservedOpCode(frame.opCode)) {
+        fail("Unrecognized frame opcode: " + String::number(frame.opCode));
+        return false;
+    }
+
+    if (frame.reserved1 || frame.reserved2 || frame.reserved3) {
+        fail("One or more reserved bits are on: reserved1 = " + String::number(frame.reserved1) + ", reserved2 = " + String::number(frame.reserved2) + ", reserved3 = " + String::number(frame.reserved3));
+        return false;
+    }
+
+    // All control frames must not be fragmented.
+    if (isControlOpCode(frame.opCode) && !frame.final) {
+        fail("Received fragmented control frame: opcode = " + String::number(frame.opCode));
+        return false;
+    }
+
+    // All control frames must have a payload of 125 bytes or less, which means the frame must not contain
+    // the "extended payload length" field.
+    if (isControlOpCode(frame.opCode) && frame.payloadLength > maxPayloadLengthWithoutExtendedLengthField) {
+        fail("Received control frame having too long payload: " + String::number(frame.payloadLength) + " bytes");
+        return false;
+    }
+
+    // A new data frame is received before the previous continuous frame finishes.
+    // Note that control frames are allowed to come in the middle of continuous frames.
+    if (m_hasContinuousFrame && frame.opCode != OpCodeContinuation && !isControlOpCode(frame.opCode)) {
+        fail("Received new data frame but previous continuous frame is unfinished.");
+        return false;
+    }
+
+    switch (frame.opCode) {
+    case OpCodeContinuation:
+        // Throw away content of a binary message because binary messages are not supported yet.
+        if (m_continuousFrameOpCode == OpCodeText)
+            m_continuousFrameData.append(frame.payload, frame.payloadLength);
+        skipBuffer(frame.frameEnd - m_buffer);
+        if (frame.final) {
+            // onmessage handler may eventually call the other methods of this channel,
+            // so we should pretend that we have finished to read this frame and
+            // make sure that the member variables are in a consistent state before
+            // the handler is invoked.
+            // Vector<char>::swap() is used here to clear m_continuousFrameData.
+            Vector<char> continuousFrameData;
+            m_continuousFrameData.swap(continuousFrameData);
+            m_hasContinuousFrame = false;
+            if (m_continuousFrameOpCode == OpCodeText) {
+                String message = String::fromUTF8(continuousFrameData.data(), continuousFrameData.size());
+                if (message.isNull())
+                    fail("Could not decode a text frame as UTF-8.");
+                else
+                    m_client->didReceiveMessage(message);
+            } else if (m_continuousFrameOpCode == OpCodeBinary) {
+                ASSERT(m_continuousFrameData.isEmpty());
+                fail("Received a binary frame which is not supported yet.");
+            }
+        }
+        break;
+
+    case OpCodeText:
+        if (frame.final) {
+            String message = String::fromUTF8(frame.payload, frame.payloadLength);
+            skipBuffer(frame.frameEnd - m_buffer);
+            if (message.isNull())
+                fail("Could not decode a text frame as UTF-8.");
+            else
+                m_client->didReceiveMessage(message);
+        } else {
+            m_hasContinuousFrame = true;
+            m_continuousFrameOpCode = OpCodeText;
+            ASSERT(m_continuousFrameData.isEmpty());
+            m_continuousFrameData.append(frame.payload, frame.payloadLength);
+            skipBuffer(frame.frameEnd - m_buffer);
+        }
+        break;
+
+    case OpCodeBinary:
+        if (frame.final)
+            fail("Received a binary frame which is not supported yet.");
+        else {
+            m_hasContinuousFrame = true;
+            m_continuousFrameOpCode = OpCodeBinary;
+            ASSERT(m_continuousFrameData.isEmpty());
+            // Do not store data of a binary message to m_continuousFrameData to save memory.
+            skipBuffer(frame.frameEnd - m_buffer);
+        }
+        break;
+
+    case OpCodeClose:
+        // FIXME: Handle payload.
+        skipBuffer(frame.frameEnd - m_buffer);
+        m_receivedClosingHandshake = true;
+        startClosingHandshake();
+        if (m_closing)
+            m_handle->close(); // Close after sending a close frame.
+        break;
+
+    case OpCodePing: {
+        bool result = sendFrame(OpCodePong, frame.payload, frame.payloadLength);
+        skipBuffer(frame.frameEnd - m_buffer);
+        if (!result)
+            fail("Failed to send a pong frame.");
+        break;
+    }
+
+    case OpCodePong:
+        // A server may send a pong in response to our ping, or an unsolicited pong which is not associated with
+        // any specific ping. Either way, there's nothing to do on receipt of pong.
+        skipBuffer(frame.frameEnd - m_buffer);
+        break;
+
+    default:
+        ASSERT_NOT_REACHED();
+        skipBuffer(frame.frameEnd - m_buffer);
+        break;
+    }
+
+    return m_buffer;
+}
+
+bool WebSocketChannel::processFrameHixie76()
+{
     const char* nextFrame = m_buffer;
     const char* p = m_buffer;
     const char* end = p + m_bufferSize;
@@ -354,7 +635,7 @@ bool WebSocketChannel::processBuffer()
         size_t length = 0;
         bool errorFrame = false;
         while (p < end) {
-            if (length > std::numeric_limits<size_t>::max() / 128) {
+            if (length > numeric_limits<size_t>::max() / 128) {
                 LOG(Network, "frame length overflow %lu", static_cast<unsigned long>(length));
                 errorFrame = true;
                 break;
@@ -362,7 +643,7 @@ bool WebSocketChannel::processBuffer()
             size_t newLength = length * 128;
             unsigned char msgByte = static_cast<unsigned char>(*p);
             unsigned int lengthMsgByte = msgByte & 0x7f;
-            if (newLength > std::numeric_limits<size_t>::max() - lengthMsgByte) {
+            if (newLength > numeric_limits<size_t>::max() - lengthMsgByte) {
                 LOG(Network, "frame length overflow %lu+%u", static_cast<unsigned long>(newLength), lengthMsgByte);
                 errorFrame = true;
                 break;
@@ -427,42 +708,56 @@ bool WebSocketChannel::processBuffer()
     return false;
 }
 
-void WebSocketChannel::resumeTimerFired(Timer<WebSocketChannel>* timer)
+bool WebSocketChannel::sendFrame(OpCode opCode, const char* data, size_t dataLength)
 {
-    ASSERT_UNUSED(timer, timer == &m_resumeTimer);
+    ASSERT(m_handle);
+    ASSERT(!m_suspended);
 
-    RefPtr<WebSocketChannel> protect(this); // The client can close the channel, potentially removing the last reference.
-    while (!m_suspended && m_client && m_buffer)
-        if (!processBuffer())
-            break;
-    if (!m_suspended && m_client && m_closed && m_handle)
-        didClose(m_handle.get());
+    Vector<char> frame;
+    ASSERT(!(opCode & ~opCodeMask)); // Checks whether "opCode" fits in the range of opCodes.
+    frame.append(finalBit | opCode);
+    if (dataLength <= maxPayloadLengthWithoutExtendedLengthField)
+        frame.append(maskBit | dataLength);
+    else if (dataLength <= 0xFFFF) {
+        frame.append(maskBit | payloadLengthWithTwoByteExtendedLengthField);
+        frame.append((dataLength & 0xFF00) >> 8);
+        frame.append(dataLength & 0xFF);
+    } else {
+        frame.append(maskBit | payloadLengthWithEightByteExtendedLengthField);
+        char extendedPayloadLength[8];
+        size_t remaining = dataLength;
+        // Fill the length into extendedPayloadLength in the network byte order.
+        for (int i = 0; i < 8; ++i) {
+            extendedPayloadLength[7 - i] = remaining & 0xFF;
+            remaining >>= 8;
+        }
+        ASSERT(!remaining);
+        frame.append(extendedPayloadLength, 8);
+    }
+
+    // Mask the frame.
+    size_t maskingKeyStart = frame.size();
+    frame.grow(frame.size() + maskingKeyWidthInBytes); // Add placeholder for masking key. Will be overwritten.
+    size_t payloadStart = frame.size();
+    frame.append(data, dataLength);
+
+    cryptographicallyRandomValues(frame.data() + maskingKeyStart, maskingKeyWidthInBytes);
+    for (size_t i = 0; i < dataLength; ++i)
+        frame[payloadStart + i] ^= frame[maskingKeyStart + i % maskingKeyWidthInBytes];
+
+    return m_handle->send(frame.data(), frame.size());
 }
 
-void WebSocketChannel::startClosingHandshake()
+bool WebSocketChannel::sendFrameHixie76(const char* data, size_t dataLength)
 {
-    LOG(Network, "WebSocketChannel %p closing %d %d", this, m_closing, m_receivedClosingHandshake);
-    if (m_closing)
-        return;
     ASSERT(m_handle);
-    Vector<char> buf;
-    buf.append('\xff');
-    buf.append('\0');
-    if (!m_handle->send(buf.data(), buf.size())) {
-        m_handle->disconnect();
-        return;
-    }
-    m_closing = true;
-    if (m_client)
-        m_client->didStartClosingHandshake();
-}
+    ASSERT(!m_suspended);
 
-void WebSocketChannel::closingTimerFired(Timer<WebSocketChannel>* timer)
-{
-    LOG(Network, "WebSocketChannel %p closing timer", this);
-    ASSERT_UNUSED(timer, &m_closingTimer == timer);
-    if (m_handle)
-        m_handle->disconnect();
+    Vector<char> frame;
+    frame.append('\0'); // Frame type.
+    frame.append(data, dataLength);
+    frame.append('\xff'); // Frame end.
+    return m_handle->send(frame.data(), frame.size());
 }
 
 }  // namespace WebCore
index 6421feb8ce0435d0449c15d8dd3b83239cac20a8..f7a470e13d2d06de8a74b96af583b55d38511307 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 Google Inc.  All rights reserved.
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -88,6 +88,45 @@ namespace WebCore {
         void startClosingHandshake();
         void closingTimerFired(Timer<WebSocketChannel>*);
 
+        // Hybi-10 opcodes.
+        typedef unsigned int OpCode;
+        static const OpCode OpCodeContinuation;
+        static const OpCode OpCodeText;
+        static const OpCode OpCodeBinary;
+        static const OpCode OpCodeClose;
+        static const OpCode OpCodePing;
+        static const OpCode OpCodePong;
+
+        static bool isNonControlOpCode(OpCode opCode) { return opCode == OpCodeContinuation || opCode == OpCodeText || opCode == OpCodeBinary; }
+        static bool isControlOpCode(OpCode opCode) { return opCode == OpCodeClose || opCode == OpCodePing || opCode == OpCodePong; }
+        static bool isReservedOpCode(OpCode opCode) { return !isNonControlOpCode(opCode) && !isControlOpCode(opCode); }
+
+        enum ParseFrameResult {
+            FrameOK,
+            FrameIncomplete,
+            FrameError
+        };
+
+        struct FrameData {
+            OpCode opCode;
+            bool final;
+            bool reserved1;
+            bool reserved2;
+            bool reserved3;
+            bool masked;
+            const char* payload;
+            size_t payloadLength;
+            const char* frameEnd;
+        };
+
+        ParseFrameResult parseFrame(FrameData&); // May modify part of m_buffer to unmask the frame.
+
+        bool processFrame();
+        bool processFrameHixie76();
+
+        bool sendFrame(OpCode, const char* data, size_t dataLength);
+        bool sendFrameHixie76(const char* data, size_t dataLength);
+
         ScriptExecutionContext* m_context;
         WebSocketChannelClient* m_client;
         OwnPtr<WebSocketHandshake> m_handshake;
@@ -107,6 +146,11 @@ namespace WebCore {
         unsigned long m_identifier; // m_identifier == 0 means that we could not obtain a valid identifier.
 
         bool m_useHixie76Protocol;
+
+        // Private members only for hybi-10 protocol.
+        bool m_hasContinuousFrame;
+        OpCode m_continuousFrameOpCode;
+        Vector<char> m_continuousFrameData;
     };
 
 } // namespace WebCore