Merge pull request #85 from veebs/WebSocketV17
Support for WebSocket Hybi V17 and Autobahn test V.0.4.3
This commit is contained in:
commit
7b71d43688
@ -32,20 +32,23 @@
|
|||||||
*
|
*
|
||||||
* <p>06. Go to AutoBahn directory: <tt>cd Autobahn</tt>
|
* <p>06. Go to AutoBahn directory: <tt>cd Autobahn</tt>
|
||||||
*
|
*
|
||||||
* <p>07. Checkout stable version: <tt>git checkout v0.4.2</tt>
|
* <p>07. Checkout stable version: <tt>git checkout v0.4.3</tt>
|
||||||
*
|
*
|
||||||
* <p>08. Go to test suite directory: <tt>cd testsuite/websockets</tt>
|
* <p>08. Go to test suite directory: <tt>cd testsuite/websockets</tt>
|
||||||
*
|
*
|
||||||
* <p>09. Edit <tt>fuzzing_clinet_spec.json</tt> and set the version to 10.
|
* <p>09. Edit <tt>fuzzing_clinet_spec.json</tt> and set the version to 10 or 17.
|
||||||
* <code>
|
* <code>
|
||||||
* {
|
* {
|
||||||
* "servers": [{"agent": "Netty", "hostname": "localhost", "port": 9000, "version": 10}],
|
* "options": {"failByDrop": false},
|
||||||
* "cases": ["*"]
|
* "servers": [{"agent": "Netty", "url": "ws://localhost:9000", "options": {"version": 17}}],
|
||||||
|
* "cases": ["*"],
|
||||||
|
* "exclude-cases": [],
|
||||||
|
* "exclude-agent-cases": {"FoobarServer*": ["4.*", "1.1.3"]}
|
||||||
* }
|
* }
|
||||||
* </code>
|
* </code>
|
||||||
*
|
*
|
||||||
* <p>10. Run the test <tt>python fuzzing_client.py</tt>. Note that the actual test case python code is
|
* <p>10. Run the test <tt>python fuzzing_client.py</tt>. Note that the actual test case python code is
|
||||||
* located in <tt>/usr/local/lib/python2.6/dist-packages/autobahn-0.4.2-py2.6.egg/autobahn/cases</tt>
|
* located in <tt>/usr/local/lib/python2.6/dist-packages/autobahn-0.4.3-py2.6.egg/autobahn/cases</tt>
|
||||||
* and not in the checked out git repository.
|
* and not in the checked out git repository.
|
||||||
*
|
*
|
||||||
* <p>11. See the results in <tt>reports/servers/index.html</tt>
|
* <p>11. See the results in <tt>reports/servers/index.html</tt>
|
||||||
|
@ -59,10 +59,10 @@ public class App {
|
|||||||
MyCallbackHandler callbackHandler = new MyCallbackHandler();
|
MyCallbackHandler callbackHandler = new MyCallbackHandler();
|
||||||
WebSocketClientFactory factory = new WebSocketClientFactory();
|
WebSocketClientFactory factory = new WebSocketClientFactory();
|
||||||
|
|
||||||
// Connect with spec version 10 (try changing it to V00 and it will
|
// Connect with spec version 17 (try changing it to V10 or V00 and it will
|
||||||
// still work ... fingers crossed ;-)
|
// still work ... fingers crossed ;-)
|
||||||
WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"),
|
WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"),
|
||||||
WebSocketSpecificationVersion.V10, callbackHandler);
|
WebSocketSpecificationVersion.V17, callbackHandler);
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
System.out.println("WebSocket Client connecting");
|
System.out.println("WebSocket Client connecting");
|
||||||
|
@ -103,7 +103,8 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) throws Exception {
|
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
// Discard all data received if closing handshake was received before.
|
// Discard all data received if closing handshake was received before.
|
||||||
if (receivedClosingHandshake) {
|
if (receivedClosingHandshake) {
|
||||||
@ -123,7 +124,9 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
frameRsv = (b & 0x70) >> 4;
|
frameRsv = (b & 0x70) >> 4;
|
||||||
frameOpcode = (b & 0x0F);
|
frameOpcode = (b & 0x0F);
|
||||||
|
|
||||||
logger.debug("Decoding WebSocket Frame opCode=" + frameOpcode);
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Decoding WebSocket Frame opCode=" + frameOpcode);
|
||||||
|
}
|
||||||
|
|
||||||
// MASK, PAYLOAD LEN 1
|
// MASK, PAYLOAD LEN 1
|
||||||
b = buffer.readByte();
|
b = buffer.readByte();
|
||||||
@ -160,9 +163,8 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// close frame : if there is a body, the first two bytes of the
|
// close frame : if there is a body, the first two bytes of the
|
||||||
// body MUST be a 2-byte
|
// body MUST be a 2-byte unsigned integer (in network byte
|
||||||
// unsigned integer (in network byte order) representing a
|
// order) representing a status code
|
||||||
// status code
|
|
||||||
if (frameOpcode == 8 && framePayloadLen1 == 1) {
|
if (frameOpcode == 8 && framePayloadLen1 == 1) {
|
||||||
protocolViolation(channel, "received close control frame with payload len 1");
|
protocolViolation(channel, "received close control frame with payload len 1");
|
||||||
return null;
|
return null;
|
||||||
@ -187,6 +189,7 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read frame payload length
|
||||||
if (framePayloadLen1 == 126) {
|
if (framePayloadLen1 == 126) {
|
||||||
framePayloadLength = buffer.readUnsignedShort();
|
framePayloadLength = buffer.readUnsignedShort();
|
||||||
if (framePayloadLength < 126) {
|
if (framePayloadLength < 126) {
|
||||||
@ -206,7 +209,10 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
framePayloadLength = framePayloadLen1;
|
framePayloadLength = framePayloadLen1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// logger.debug("Frame length=" + framePayloadLength);
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Decoding WebSocket Frame length=" + framePayloadLength);
|
||||||
|
}
|
||||||
|
|
||||||
checkpoint(State.MASKING_KEY);
|
checkpoint(State.MASKING_KEY);
|
||||||
case MASKING_KEY:
|
case MASKING_KEY:
|
||||||
if (this.maskedPayload) {
|
if (this.maskedPayload) {
|
||||||
@ -214,7 +220,7 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
}
|
}
|
||||||
checkpoint(State.PAYLOAD);
|
checkpoint(State.PAYLOAD);
|
||||||
case PAYLOAD:
|
case PAYLOAD:
|
||||||
// Some times, the payload may not be delivered in 1 nice packet
|
// Sometimes, the payload may not be delivered in 1 nice packet
|
||||||
// We need to accumulate the data until we have it all
|
// We need to accumulate the data until we have it all
|
||||||
int rbytes = actualReadableBytes();
|
int rbytes = actualReadableBytes();
|
||||||
ChannelBuffer payloadBuffer = null;
|
ChannelBuffer payloadBuffer = null;
|
||||||
@ -260,7 +266,19 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
unmask(framePayload);
|
unmask(framePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processing for fragmented messages
|
// Processing ping/pong/close frames because they cannot be
|
||||||
|
// fragmented
|
||||||
|
if (frameOpcode == OPCODE_PING) {
|
||||||
|
return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
||||||
|
} else if (frameOpcode == OPCODE_PONG) {
|
||||||
|
return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
||||||
|
} else if (frameOpcode == OPCODE_CLOSE) {
|
||||||
|
this.receivedClosingHandshake = true;
|
||||||
|
return new CloseWebSocketFrame(frameFinalFlag, frameRsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing for possible fragmented messages for text and binary
|
||||||
|
// frames
|
||||||
String aggregatedText = null;
|
String aggregatedText = null;
|
||||||
if (frameFinalFlag) {
|
if (frameFinalFlag) {
|
||||||
// Final frame of the sequence. Apparently ping frames are
|
// Final frame of the sequence. Apparently ping frames are
|
||||||
@ -305,15 +323,8 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
|
|||||||
return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
||||||
} else if (frameOpcode == OPCODE_BINARY) {
|
} else if (frameOpcode == OPCODE_BINARY) {
|
||||||
return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
||||||
} else if (frameOpcode == OPCODE_PING) {
|
|
||||||
return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
|
||||||
} else if (frameOpcode == OPCODE_PONG) {
|
|
||||||
return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
|
|
||||||
} else if (frameOpcode == OPCODE_CONT) {
|
} else if (frameOpcode == OPCODE_CONT) {
|
||||||
return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText);
|
return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText);
|
||||||
} else if (frameOpcode == OPCODE_CLOSE) {
|
|
||||||
this.receivedClosingHandshake = true;
|
|
||||||
return new CloseWebSocketFrame(frameFinalFlag, frameRsv);
|
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode);
|
throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode);
|
||||||
}
|
}
|
||||||
|
@ -114,8 +114,10 @@ public class WebSocket08FrameEncoder extends OneToOneEncoder {
|
|||||||
|
|
||||||
int length = data.readableBytes();
|
int length = data.readableBytes();
|
||||||
|
|
||||||
logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length);
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length);
|
||||||
|
}
|
||||||
|
|
||||||
int b0 = 0;
|
int b0 = 0;
|
||||||
if (frame.isFinalFragment()) {
|
if (frame.isFinalFragment()) {
|
||||||
b0 |= (1 << 7);
|
b0 |= (1 << 7);
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
// (BSD License: http://www.opensource.org/licenses/bsd-license)
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011, Joe Walnes and contributors
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or
|
||||||
|
// without modification, are permitted provided that the
|
||||||
|
// following conditions are met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer in the documentation and/or other
|
||||||
|
// materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// * Neither the name of the Webbit nor the names of
|
||||||
|
// its contributors may be used to endorse or promote products
|
||||||
|
// derived from this software without specific prior written
|
||||||
|
// permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||||
|
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||||
|
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
// POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
package org.jboss.netty.handler.codec.http.websocketx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a web socket frame from wire protocol version 13 format.
|
||||||
|
* V13 is essentially the same as V8.
|
||||||
|
*
|
||||||
|
* @author <a href="http://www.veebsbraindump.com/">Vibul Imtarnasan</a>
|
||||||
|
*/
|
||||||
|
public class WebSocket13FrameDecoder extends WebSocket08FrameDecoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param maskedPayload
|
||||||
|
* Web socket servers must set this to true processed incoming
|
||||||
|
* masked payload. Client implementations must set this to false.
|
||||||
|
* @param allowExtensions
|
||||||
|
* Flag to allow reserved extension bits to be used or not
|
||||||
|
*/
|
||||||
|
public WebSocket13FrameDecoder(boolean maskedPayload, boolean allowExtensions) {
|
||||||
|
super(maskedPayload, allowExtensions);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
// (BSD License: http://www.opensource.org/licenses/bsd-license)
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011, Joe Walnes and contributors
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or
|
||||||
|
// without modification, are permitted provided that the
|
||||||
|
// following conditions are met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer in the documentation and/or other
|
||||||
|
// materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// * Neither the name of the Webbit nor the names of
|
||||||
|
// its contributors may be used to endorse or promote products
|
||||||
|
// derived from this software without specific prior written
|
||||||
|
// permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||||
|
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||||
|
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
// POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
package org.jboss.netty.handler.codec.http.websocketx;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Encodes a web socket frame into wire protocol version 13 format. V13 is essentially the same
|
||||||
|
* as V8.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="http://www.veebsbraindump.com/">Vibul Imtarnasan</a>
|
||||||
|
*/
|
||||||
|
public class WebSocket13FrameEncoder extends WebSocket08FrameEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param maskPayload
|
||||||
|
* Web socket clients must set this to true to mask payload.
|
||||||
|
* Server implementations must set this to false.
|
||||||
|
*/
|
||||||
|
public WebSocket13FrameEncoder(boolean maskPayload) {
|
||||||
|
super(maskPayload);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010 Red Hat, Inc.
|
||||||
|
*
|
||||||
|
* Red Hat 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.jboss.netty.handler.codec.http.websocketx;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import org.jboss.netty.channel.Channel;
|
||||||
|
import org.jboss.netty.channel.ChannelHandlerContext;
|
||||||
|
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpMethod;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpRequest;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpResponse;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpVersion;
|
||||||
|
import org.jboss.netty.logging.InternalLogger;
|
||||||
|
import org.jboss.netty.logging.InternalLoggerFactory;
|
||||||
|
import org.jboss.netty.util.CharsetUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Performs client side opening and closing handshakes for web socket
|
||||||
|
* specification version <a
|
||||||
|
* href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17"
|
||||||
|
* >draft-ietf-hybi-thewebsocketprotocol- 17</a>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
|
||||||
|
*/
|
||||||
|
public class WebSocketClientHandshaker17 extends WebSocketClientHandshaker {
|
||||||
|
|
||||||
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker17.class);
|
||||||
|
|
||||||
|
public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
|
||||||
|
private String expectedChallengeResponseString = null;
|
||||||
|
|
||||||
|
private static final String protocol = null;
|
||||||
|
|
||||||
|
private boolean allowExtensions = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor specifying the destination web socket location and version to
|
||||||
|
* initiate
|
||||||
|
*
|
||||||
|
* @param webSocketURL
|
||||||
|
* URL for web socket communications. e.g
|
||||||
|
* "ws://myhost.com/mypath". Subsequent web socket frames will be
|
||||||
|
* sent to this URL.
|
||||||
|
* @param version
|
||||||
|
* Version of web socket specification to use to connect to the
|
||||||
|
* server
|
||||||
|
* @param subProtocol
|
||||||
|
* Sub protocol request sent to the server.
|
||||||
|
* @param allowExtensions
|
||||||
|
* Allow extensions to be used in the reserved bits of the web
|
||||||
|
* socket frame
|
||||||
|
*/
|
||||||
|
public WebSocketClientHandshaker17(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, boolean allowExtensions) {
|
||||||
|
super(webSocketURL, version, subProtocol);
|
||||||
|
this.allowExtensions = allowExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /**
|
||||||
|
* <p>
|
||||||
|
* Sends the opening request to the server:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* GET /chat HTTP/1.1
|
||||||
|
* Host: server.example.com
|
||||||
|
* Upgrade: websocket
|
||||||
|
* Connection: Upgrade
|
||||||
|
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
||||||
|
* Sec-WebSocket-Origin: http://example.com
|
||||||
|
* Sec-WebSocket-Protocol: chat, superchat
|
||||||
|
* Sec-WebSocket-Version: 13
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param ctx
|
||||||
|
* Channel context
|
||||||
|
* @param channel
|
||||||
|
* Channel into which we can write our request
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void beginOpeningHandshake(ChannelHandlerContext ctx, Channel channel) {
|
||||||
|
// Get path
|
||||||
|
URI wsURL = this.getWebSocketURL();
|
||||||
|
String path = wsURL.getPath();
|
||||||
|
if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
|
||||||
|
path = wsURL.getPath() + "?" + wsURL.getQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 16 bit nonce and base 64 encode it
|
||||||
|
byte[] nonce = createRandomBytes(16);
|
||||||
|
String key = base64Encode(nonce);
|
||||||
|
|
||||||
|
String acceptSeed = key + MAGIC_GUID;
|
||||||
|
byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
|
||||||
|
this.expectedChallengeResponseString = base64Encode(sha1);
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug(String.format("HyBi17 Client Handshake key: %s. Expected response: %s.", key, this.expectedChallengeResponseString));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format request
|
||||||
|
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
|
||||||
|
request.addHeader(Names.UPGRADE, Values.WEBSOCKET.toLowerCase());
|
||||||
|
request.addHeader(Names.CONNECTION, Values.UPGRADE);
|
||||||
|
request.addHeader(Names.SEC_WEBSOCKET_KEY, key);
|
||||||
|
request.addHeader(Names.HOST, wsURL.getHost());
|
||||||
|
request.addHeader(Names.ORIGIN, "http://" + wsURL.getHost());
|
||||||
|
if (protocol != null && !protocol.equals("")) {
|
||||||
|
request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol);
|
||||||
|
}
|
||||||
|
request.addHeader(Names.SEC_WEBSOCKET_VERSION, "13");
|
||||||
|
|
||||||
|
channel.write(request);
|
||||||
|
|
||||||
|
ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket13FrameEncoder(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Process server response:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* HTTP/1.1 101 Switching Protocols
|
||||||
|
* Upgrade: websocket
|
||||||
|
* Connection: Upgrade
|
||||||
|
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
||||||
|
* Sec-WebSocket-Protocol: chat
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param ctx
|
||||||
|
* Channel context
|
||||||
|
* @param response
|
||||||
|
* HTTP response returned from the server for the request sent by
|
||||||
|
* beginOpeningHandshake00().
|
||||||
|
* @throws WebSocketHandshakeException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response) throws WebSocketHandshakeException {
|
||||||
|
final HttpResponseStatus status = new HttpResponseStatus(101, "Switching Protocols");
|
||||||
|
|
||||||
|
if (!response.getStatus().equals(status)) {
|
||||||
|
throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
String upgrade = response.getHeader(Names.UPGRADE);
|
||||||
|
if (upgrade == null || !upgrade.equals(Values.WEBSOCKET.toLowerCase())) {
|
||||||
|
throw new WebSocketHandshakeException("Invalid handshake response upgrade: " + response.getHeader(Names.UPGRADE));
|
||||||
|
}
|
||||||
|
|
||||||
|
String connection = response.getHeader(Names.CONNECTION);
|
||||||
|
if (connection == null || !connection.equals(Values.UPGRADE)) {
|
||||||
|
throw new WebSocketHandshakeException("Invalid handshake response connection: " + response.getHeader(Names.CONNECTION));
|
||||||
|
}
|
||||||
|
|
||||||
|
String accept = response.getHeader(Names.SEC_WEBSOCKET_ACCEPT);
|
||||||
|
if (accept == null || !accept.equals(this.expectedChallengeResponseString)) {
|
||||||
|
throw new WebSocketHandshakeException(String.format("Invalid challenge. Actual: %s. Expected: %s", accept, this.expectedChallengeResponseString));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.getPipeline().replace("decoder", "ws-decoder", new WebSocket13FrameDecoder(false, this.allowExtensions));
|
||||||
|
|
||||||
|
this.setOpenningHandshakeCompleted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -43,6 +43,9 @@ public class WebSocketClientHandshakerFactory {
|
|||||||
* @throws WebSocketHandshakeException
|
* @throws WebSocketHandshakeException
|
||||||
*/
|
*/
|
||||||
public WebSocketClientHandshaker newHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, boolean allowExtensions) throws WebSocketHandshakeException {
|
public WebSocketClientHandshaker newHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, boolean allowExtensions) throws WebSocketHandshakeException {
|
||||||
|
if (version == WebSocketSpecificationVersion.V17) {
|
||||||
|
return new WebSocketClientHandshaker17(webSocketURL, version, subProtocol, allowExtensions);
|
||||||
|
}
|
||||||
if (version == WebSocketSpecificationVersion.V10) {
|
if (version == WebSocketSpecificationVersion.V10) {
|
||||||
return new WebSocketClientHandshaker10(webSocketURL, version, subProtocol, allowExtensions);
|
return new WebSocketClientHandshaker10(webSocketURL, version, subProtocol, allowExtensions);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
|||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import org.jboss.netty.channel.ChannelFuture;
|
||||||
|
import org.jboss.netty.channel.ChannelFutureListener;
|
||||||
import org.jboss.netty.channel.ChannelHandlerContext;
|
import org.jboss.netty.channel.ChannelHandlerContext;
|
||||||
import org.jboss.netty.channel.ChannelPipeline;
|
import org.jboss.netty.channel.ChannelPipeline;
|
||||||
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
||||||
@ -150,7 +152,7 @@ public class WebSocketServerHandshaker10 extends WebSocketServerHandshaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Echo back the closing frame
|
* Echo back the closing frame and close the connection
|
||||||
*
|
*
|
||||||
* @param ctx
|
* @param ctx
|
||||||
* Channel context
|
* Channel context
|
||||||
@ -159,7 +161,8 @@ public class WebSocketServerHandshaker10 extends WebSocketServerHandshaker {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
|
public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
|
||||||
ctx.getChannel().write(frame);
|
ChannelFuture f = ctx.getChannel().write(frame);
|
||||||
|
f.addListener(ChannelFutureListener.CLOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010 Red Hat, Inc.
|
||||||
|
*
|
||||||
|
* Red Hat 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.jboss.netty.handler.codec.http.websocketx;
|
||||||
|
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import org.jboss.netty.channel.ChannelFuture;
|
||||||
|
import org.jboss.netty.channel.ChannelFutureListener;
|
||||||
|
import org.jboss.netty.channel.ChannelHandlerContext;
|
||||||
|
import org.jboss.netty.channel.ChannelPipeline;
|
||||||
|
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpRequest;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpResponse;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
|
||||||
|
import org.jboss.netty.logging.InternalLogger;
|
||||||
|
import org.jboss.netty.logging.InternalLoggerFactory;
|
||||||
|
import org.jboss.netty.util.CharsetUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Performs server side opening and closing handshakes for web socket
|
||||||
|
* specification version <a
|
||||||
|
* href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17"
|
||||||
|
* >draft-ietf-hybi-thewebsocketprotocol- 17</a>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
|
||||||
|
*/
|
||||||
|
public class WebSocketServerHandshaker17 extends WebSocketServerHandshaker {
|
||||||
|
|
||||||
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker17.class);
|
||||||
|
|
||||||
|
public static final String WEBSOCKET_17_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
|
||||||
|
private boolean allowExtensions = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor specifying the destination web socket location
|
||||||
|
*
|
||||||
|
* @param webSocketURL
|
||||||
|
* URL for web socket communications. e.g
|
||||||
|
* "ws://myhost.com/mypath". Subsequent web socket frames will be
|
||||||
|
* sent to this URL.
|
||||||
|
* @param subProtocols
|
||||||
|
* CSV of supported protocols
|
||||||
|
* @param allowExtensions
|
||||||
|
* Allow extensions to be used in the reserved bits of the web
|
||||||
|
* socket frame
|
||||||
|
*/
|
||||||
|
public WebSocketServerHandshaker17(String webSocketURL, String subProtocols, boolean allowExtensions) {
|
||||||
|
super(webSocketURL, subProtocols);
|
||||||
|
this.allowExtensions = allowExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Handle the web socket handshake for the web socket specification <a href=
|
||||||
|
* "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17">HyBi
|
||||||
|
* versions 13-17</a>. Versions 13-17 share the same wire protocol.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Browser request to the server:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* GET /chat HTTP/1.1
|
||||||
|
* Host: server.example.com
|
||||||
|
* Upgrade: websocket
|
||||||
|
* Connection: Upgrade
|
||||||
|
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
||||||
|
* Sec-WebSocket-Origin: http://example.com
|
||||||
|
* Sec-WebSocket-Protocol: chat, superchat
|
||||||
|
* Sec-WebSocket-Version: 13
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Server response:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* HTTP/1.1 101 Switching Protocols
|
||||||
|
* Upgrade: websocket
|
||||||
|
* Connection: Upgrade
|
||||||
|
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
||||||
|
* Sec-WebSocket-Protocol: chat
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param ctx
|
||||||
|
* Channel context
|
||||||
|
* @param req
|
||||||
|
* HTTP request
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req) {
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug(String.format("Channel %s web socket spec version 17 handshake", ctx.getChannel().getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse res = new DefaultHttpResponse(HTTP_1_1, new HttpResponseStatus(101, "Switching Protocols"));
|
||||||
|
this.setVersion(WebSocketSpecificationVersion.V17);
|
||||||
|
|
||||||
|
String key = req.getHeader(Names.SEC_WEBSOCKET_KEY);
|
||||||
|
if (key == null) {
|
||||||
|
res.setStatus(HttpResponseStatus.BAD_REQUEST);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String acceptSeed = key + WEBSOCKET_17_ACCEPT_GUID;
|
||||||
|
byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
|
||||||
|
String accept = base64Encode(sha1);
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug(String.format("HyBi17 Server Handshake key: %s. Response: %s.", key, accept));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatus(new HttpResponseStatus(101, "Switching Protocols"));
|
||||||
|
res.addHeader(Names.UPGRADE, WEBSOCKET.toLowerCase());
|
||||||
|
res.addHeader(Names.CONNECTION, Names.UPGRADE);
|
||||||
|
res.addHeader(Names.SEC_WEBSOCKET_ACCEPT, accept);
|
||||||
|
String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
|
||||||
|
if (protocol != null) {
|
||||||
|
res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, this.selectSubProtocol(protocol));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.getChannel().write(res);
|
||||||
|
|
||||||
|
// Upgrade the connection and send the handshake response.
|
||||||
|
ChannelPipeline p = ctx.getChannel().getPipeline();
|
||||||
|
p.remove("aggregator");
|
||||||
|
p.replace("decoder", "wsdecoder", new WebSocket13FrameDecoder(true, this.allowExtensions));
|
||||||
|
p.replace("encoder", "wsencoder", new WebSocket13FrameEncoder(false));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo back the closing frame and close the connection
|
||||||
|
*
|
||||||
|
* @param ctx
|
||||||
|
* Channel context
|
||||||
|
* @param frame
|
||||||
|
* Web Socket frame that was received
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
|
||||||
|
ChannelFuture f = ctx.getChannel().write(frame);
|
||||||
|
f.addListener(ChannelFutureListener.CLOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -30,66 +30,71 @@ import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
|
|||||||
*/
|
*/
|
||||||
public class WebSocketServerHandshakerFactory {
|
public class WebSocketServerHandshakerFactory {
|
||||||
|
|
||||||
private final String webSocketURL;
|
private final String webSocketURL;
|
||||||
|
|
||||||
private final String subProtocols;
|
private final String subProtocols;
|
||||||
|
|
||||||
private boolean allowExtensions = false;
|
private boolean allowExtensions = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor specifying the destination web socket location
|
* Constructor specifying the destination web socket location
|
||||||
*
|
*
|
||||||
* @param webSocketURL
|
* @param webSocketURL
|
||||||
* URL for web socket communications. e.g
|
* URL for web socket communications. e.g
|
||||||
* "ws://myhost.com/mypath". Subsequent web socket frames will be
|
* "ws://myhost.com/mypath". Subsequent web socket frames will be
|
||||||
* sent to this URL.
|
* sent to this URL.
|
||||||
* @param subProtocols
|
* @param subProtocols
|
||||||
* CSV of supported protocols. Null if sub protocols not
|
* CSV of supported protocols. Null if sub protocols not
|
||||||
* supported.
|
* supported.
|
||||||
* @param allowExtensions
|
* @param allowExtensions
|
||||||
* Allow extensions to be used in the reserved bits of the web
|
* Allow extensions to be used in the reserved bits of the web
|
||||||
* socket frame
|
* socket frame
|
||||||
*/
|
*/
|
||||||
public WebSocketServerHandshakerFactory(String webSocketURL, String subProtocols, boolean allowExtensions) {
|
public WebSocketServerHandshakerFactory(String webSocketURL, String subProtocols, boolean allowExtensions) {
|
||||||
this.webSocketURL = webSocketURL;
|
this.webSocketURL = webSocketURL;
|
||||||
this.subProtocols = subProtocols;
|
this.subProtocols = subProtocols;
|
||||||
this.allowExtensions = allowExtensions;
|
this.allowExtensions = allowExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instances a new handshaker
|
* Instances a new handshaker
|
||||||
*
|
*
|
||||||
* @return A new WebSocketServerHandshaker for the requested web socket
|
* @return A new WebSocketServerHandshaker for the requested web socket
|
||||||
* version. Null if web socket version is not supported.
|
* version. Null if web socket version is not supported.
|
||||||
*/
|
*/
|
||||||
public WebSocketServerHandshaker newHandshaker(ChannelHandlerContext ctx, HttpRequest req) {
|
public WebSocketServerHandshaker newHandshaker(ChannelHandlerContext ctx, HttpRequest req) {
|
||||||
|
|
||||||
String version = req.getHeader(Names.SEC_WEBSOCKET_VERSION);
|
String version = req.getHeader(Names.SEC_WEBSOCKET_VERSION);
|
||||||
if (version != null) {
|
if (version != null) {
|
||||||
if (version.equals("8")) {
|
if (version.equals("13")) {
|
||||||
// Version 8 of the wire protocol - assume version 10 of the
|
// Version 13 of the wire protocol - assume version 17 of the
|
||||||
// specification.
|
// specification.
|
||||||
return new WebSocketServerHandshaker10(webSocketURL, subProtocols, this.allowExtensions);
|
return new WebSocketServerHandshaker17(webSocketURL, subProtocols, this.allowExtensions);
|
||||||
} else {
|
} else if (version.equals("8")) {
|
||||||
return null;
|
// Version 8 of the wire protocol - assume version 10 of the
|
||||||
}
|
// specification.
|
||||||
} else {
|
return new WebSocketServerHandshaker10(webSocketURL, subProtocols, this.allowExtensions);
|
||||||
// Assume version 00 where version header was not specified
|
} else {
|
||||||
return new WebSocketServerHandshaker00(webSocketURL, subProtocols);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Assume version 00 where version header was not specified
|
||||||
|
return new WebSocketServerHandshaker00(webSocketURL, subProtocols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return that we need cannot not support the web socket version
|
* Return that we need cannot not support the web socket version
|
||||||
*
|
*
|
||||||
* @param ctx
|
* @param ctx
|
||||||
* Context
|
* Context
|
||||||
*/
|
*/
|
||||||
public void sendUnsupportedWebSocketVersionResponse(ChannelHandlerContext ctx) {
|
public void sendUnsupportedWebSocketVersionResponse(ChannelHandlerContext ctx) {
|
||||||
HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Switching Protocols"));
|
HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101,
|
||||||
res.setStatus(HttpResponseStatus.UPGRADE_REQUIRED);
|
"Switching Protocols"));
|
||||||
res.setHeader(Names.SEC_WEBSOCKET_VERSION, "8");
|
res.setStatus(HttpResponseStatus.UPGRADE_REQUIRED);
|
||||||
ctx.getChannel().write(res);
|
res.setHeader(Names.SEC_WEBSOCKET_VERSION, "13");
|
||||||
}
|
ctx.getChannel().write(res);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,5 +41,13 @@ public enum WebSocketSpecificationVersion {
|
|||||||
* "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10"
|
* "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10"
|
||||||
* >draft-ietf-hybi-thewebsocketprotocol- 10</a>
|
* >draft-ietf-hybi-thewebsocketprotocol- 10</a>
|
||||||
*/
|
*/
|
||||||
V10
|
V10,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17"
|
||||||
|
* >draft-ietf-hybi-thewebsocketprotocol- 17</a>
|
||||||
|
*/
|
||||||
|
V17
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user