Added support for Hybi V17 and run against Autobahn V0.4.3

This commit is contained in:
Veebs 2011-11-29 09:45:13 +11:00
parent 8efe131eb0
commit 48addae927
9 changed files with 835 additions and 332 deletions

View File

@ -59,317 +59,325 @@ import org.jboss.netty.logging.InternalLoggerFactory;
*/ */
public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDecoder.State> { public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDecoder.State> {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameDecoder.class); private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameDecoder.class);
private static final byte OPCODE_CONT = 0x0; private static final byte OPCODE_CONT = 0x0;
private static final byte OPCODE_TEXT = 0x1; private static final byte OPCODE_TEXT = 0x1;
private static final byte OPCODE_BINARY = 0x2; private static final byte OPCODE_BINARY = 0x2;
private static final byte OPCODE_CLOSE = 0x8; private static final byte OPCODE_CLOSE = 0x8;
private static final byte OPCODE_PING = 0x9; private static final byte OPCODE_PING = 0x9;
private static final byte OPCODE_PONG = 0xA; private static final byte OPCODE_PONG = 0xA;
private UTF8Output fragmentedFramesText = null; private UTF8Output fragmentedFramesText = null;
private int fragmentedFramesCount = 0; private int fragmentedFramesCount = 0;
private boolean frameFinalFlag; private boolean frameFinalFlag;
private int frameRsv; private int frameRsv;
private int frameOpcode; private int frameOpcode;
private long framePayloadLength; private long framePayloadLength;
private ChannelBuffer framePayload = null; private ChannelBuffer framePayload = null;
private int framePayloadBytesRead = 0; private int framePayloadBytesRead = 0;
private ChannelBuffer maskingKey; private ChannelBuffer maskingKey;
private boolean allowExtensions = false; private boolean allowExtensions = false;
private boolean maskedPayload = false; private boolean maskedPayload = false;
private boolean receivedClosingHandshake = false; private boolean receivedClosingHandshake = false;
public enum State { public enum State {
FRAME_START, MASKING_KEY, PAYLOAD, CORRUPT FRAME_START, MASKING_KEY, PAYLOAD, CORRUPT
} }
/** /**
* Constructor * Constructor
* *
* @param maskedPayload * @param maskedPayload
* Web socket servers must set this to true processed incoming * Web socket servers must set this to true processed incoming
* masked payload. Client implementations must set this to false. * masked payload. Client implementations must set this to false.
* @param allowExtensions * @param allowExtensions
* Flag to allow reserved extension bits to be used or not * Flag to allow reserved extension bits to be used or not
*/ */
public WebSocket08FrameDecoder(boolean maskedPayload, boolean allowExtensions) { public WebSocket08FrameDecoder(boolean maskedPayload, boolean allowExtensions) {
super(State.FRAME_START); super(State.FRAME_START);
this.maskedPayload = maskedPayload; this.maskedPayload = maskedPayload;
this.allowExtensions = allowExtensions; this.allowExtensions = allowExtensions;
} }
@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) {
buffer.skipBytes(actualReadableBytes()); buffer.skipBytes(actualReadableBytes());
return null; return null;
} }
switch (state) { switch (state) {
case FRAME_START: case FRAME_START:
framePayloadBytesRead = 0; framePayloadBytesRead = 0;
framePayloadLength = -1; framePayloadLength = -1;
framePayload = null; framePayload = null;
// FIN, RSV, OPCODE // FIN, RSV, OPCODE
byte b = buffer.readByte(); byte b = buffer.readByte();
frameFinalFlag = (b & 0x80) != 0; frameFinalFlag = (b & 0x80) != 0;
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();
boolean frameMasked = (b & 0x80) != 0; boolean frameMasked = (b & 0x80) != 0;
int framePayloadLen1 = (b & 0x7F); int framePayloadLen1 = (b & 0x7F);
if (frameRsv != 0 && !this.allowExtensions) { if (frameRsv != 0 && !this.allowExtensions) {
protocolViolation(channel, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); protocolViolation(channel, "RSV != 0 and no extension negotiated, RSV:" + frameRsv);
return null; return null;
} }
if (this.maskedPayload && !frameMasked) { if (this.maskedPayload && !frameMasked) {
protocolViolation(channel, "unmasked client to server frame"); protocolViolation(channel, "unmasked client to server frame");
return null; return null;
} }
if (frameOpcode > 7) { // control frame (have MSB in opcode set) if (frameOpcode > 7) { // control frame (have MSB in opcode set)
// control frames MUST NOT be fragmented // control frames MUST NOT be fragmented
if (!frameFinalFlag) { if (!frameFinalFlag) {
protocolViolation(channel, "fragmented control frame"); protocolViolation(channel, "fragmented control frame");
return null; return null;
} }
// control frames MUST have payload 125 octets or less // control frames MUST have payload 125 octets or less
if (framePayloadLen1 > 125) { if (framePayloadLen1 > 125) {
protocolViolation(channel, "control frame with payload length > 125 octets"); protocolViolation(channel, "control frame with payload length > 125 octets");
return null; return null;
} }
// check for reserved control frame opcodes // check for reserved control frame opcodes
if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) {
protocolViolation(channel, "control frame using reserved opcode " + frameOpcode); protocolViolation(channel, "control frame using reserved opcode " + frameOpcode);
return null; return null;
} }
// 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; }
} } else { // data frame
} else { // data frame // check for reserved data frame opcodes
// check for reserved data frame opcodes if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) {
if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { protocolViolation(channel, "data frame using reserved opcode " + frameOpcode);
protocolViolation(channel, "data frame using reserved opcode " + frameOpcode); return null;
return null; }
}
// check opcode vs message fragmentation state 1/2 // check opcode vs message fragmentation state 1/2
if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) {
protocolViolation(channel, "received continuation data frame outside fragmented message"); protocolViolation(channel, "received continuation data frame outside fragmented message");
return null; return null;
} }
// check opcode vs message fragmentation state 2/2 // check opcode vs message fragmentation state 2/2
if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) {
protocolViolation(channel, "received non-continuation data frame while inside fragmented message"); protocolViolation(channel, "received non-continuation data frame while inside fragmented message");
return null; return null;
} }
} }
if (framePayloadLen1 == 126) { // Read frame payload length
framePayloadLength = buffer.readUnsignedShort(); if (framePayloadLen1 == 126) {
if (framePayloadLength < 126) { framePayloadLength = buffer.readUnsignedShort();
protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); if (framePayloadLength < 126) {
return null; protocolViolation(channel, "invalid data frame length (not using minimal length encoding)");
} return null;
} else if (framePayloadLen1 == 127) { }
framePayloadLength = buffer.readLong(); } else if (framePayloadLen1 == 127) {
// TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe framePayloadLength = buffer.readLong();
// just check if it's negative? // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe
// just check if it's negative?
if (framePayloadLength < 65536) { if (framePayloadLength < 65536) {
protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); protocolViolation(channel, "invalid data frame length (not using minimal length encoding)");
return null; return null;
} }
} else { } else {
framePayloadLength = framePayloadLen1; framePayloadLength = framePayloadLen1;
} }
// logger.debug("Frame length=" + framePayloadLength); if (logger.isDebugEnabled()) {
checkpoint(State.MASKING_KEY); logger.debug("Decoding WebSocket Frame length=" + framePayloadLength);
case MASKING_KEY: }
if (this.maskedPayload) {
maskingKey = buffer.readBytes(4);
}
checkpoint(State.PAYLOAD);
case PAYLOAD:
// Some times, the payload may not be delivered in 1 nice packet
// We need to accumulate the data until we have it all
int rbytes = actualReadableBytes();
ChannelBuffer payloadBuffer = null;
int willHaveReadByteCount = framePayloadBytesRead + rbytes; checkpoint(State.MASKING_KEY);
// logger.debug("Frame rbytes=" + rbytes + " willHaveReadByteCount=" case MASKING_KEY:
// + willHaveReadByteCount + " framePayloadLength=" + if (this.maskedPayload) {
// framePayloadLength); maskingKey = buffer.readBytes(4);
if (willHaveReadByteCount == framePayloadLength) { }
// We have all our content so proceed to process checkpoint(State.PAYLOAD);
payloadBuffer = buffer.readBytes(rbytes); case PAYLOAD:
} else if (willHaveReadByteCount < framePayloadLength) { // Sometimes, the payload may not be delivered in 1 nice packet
// We don't have all our content so accumulate payload. // We need to accumulate the data until we have it all
// Returning null means we will get called back int rbytes = actualReadableBytes();
payloadBuffer = buffer.readBytes(rbytes); ChannelBuffer payloadBuffer = null;
if (framePayload == null) {
framePayload = channel.getConfig().getBufferFactory().getBuffer(toFrameLength(framePayloadLength));
}
framePayload.writeBytes(payloadBuffer);
framePayloadBytesRead = framePayloadBytesRead + rbytes;
// Return null to wait for more bytes to arrive int willHaveReadByteCount = framePayloadBytesRead + rbytes;
return null; // logger.debug("Frame rbytes=" + rbytes + " willHaveReadByteCount="
} else if (willHaveReadByteCount > framePayloadLength) { // + willHaveReadByteCount + " framePayloadLength=" +
// We have more than what we need so read up to the end of frame // framePayloadLength);
// Leave the remainder in the buffer for next frame if (willHaveReadByteCount == framePayloadLength) {
payloadBuffer = buffer.readBytes(toFrameLength(framePayloadLength - framePayloadBytesRead)); // We have all our content so proceed to process
} payloadBuffer = buffer.readBytes(rbytes);
} else if (willHaveReadByteCount < framePayloadLength) {
// We don't have all our content so accumulate payload.
// Returning null means we will get called back
payloadBuffer = buffer.readBytes(rbytes);
if (framePayload == null) {
framePayload = channel.getConfig().getBufferFactory().getBuffer(toFrameLength(framePayloadLength));
}
framePayload.writeBytes(payloadBuffer);
framePayloadBytesRead = framePayloadBytesRead + rbytes;
// Now we have all the data, the next checkpoint must be the next // Return null to wait for more bytes to arrive
// frame return null;
checkpoint(State.FRAME_START); } else if (willHaveReadByteCount > framePayloadLength) {
// We have more than what we need so read up to the end of frame
// Leave the remainder in the buffer for next frame
payloadBuffer = buffer.readBytes(toFrameLength(framePayloadLength - framePayloadBytesRead));
}
// Take the data that we have in this packet // Now we have all the data, the next checkpoint must be the next
if (framePayload == null) { // frame
framePayload = payloadBuffer; checkpoint(State.FRAME_START);
} else {
framePayload.writeBytes(payloadBuffer);
}
// Unmask data if needed // Take the data that we have in this packet
if (this.maskedPayload) { if (framePayload == null) {
unmask(framePayload); framePayload = payloadBuffer;
} } else {
framePayload.writeBytes(payloadBuffer);
}
// Processing ping/pong/close frames because they cannot be fragmented // Unmask data if needed
if (frameOpcode == OPCODE_PING) { if (this.maskedPayload) {
return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload); unmask(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;
if (frameFinalFlag) {
// Final frame of the sequence. Apparently ping frames are
// allowed in the middle of a fragmented message
if (frameOpcode != OPCODE_PING) {
fragmentedFramesCount = 0;
// Check text for UTF8 correctness // Processing ping/pong/close frames because they cannot be
if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) { // fragmented
// Check UTF-8 correctness for this payload if (frameOpcode == OPCODE_PING) {
checkUTF8String(channel, framePayload.array()); 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);
}
// This does a second check to make sure UTF-8 // Processing for possible fragmented messages for text and binary
// correctness for entire text message // frames
aggregatedText = fragmentedFramesText.toString(); String aggregatedText = null;
if (frameFinalFlag) {
// Final frame of the sequence. Apparently ping frames are
// allowed in the middle of a fragmented message
if (frameOpcode != OPCODE_PING) {
fragmentedFramesCount = 0;
fragmentedFramesText = null; // Check text for UTF8 correctness
} if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) {
} // Check UTF-8 correctness for this payload
} else { checkUTF8String(channel, framePayload.array());
// Not final frame so we can expect more frames in the
// fragmented sequence
if (fragmentedFramesCount == 0) {
// First text or binary frame for a fragmented set
fragmentedFramesText = null;
if (frameOpcode == OPCODE_TEXT) {
checkUTF8String(channel, framePayload.array());
}
} else {
// Subsequent frames - only check if init frame is text
if (fragmentedFramesText != null) {
checkUTF8String(channel, framePayload.array());
}
}
// Increment counter // This does a second check to make sure UTF-8
fragmentedFramesCount++; // correctness for entire text message
} aggregatedText = fragmentedFramesText.toString();
// Return the frame fragmentedFramesText = null;
if (frameOpcode == OPCODE_TEXT) { }
return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload); }
} else if (frameOpcode == OPCODE_BINARY) { } else {
return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload); // Not final frame so we can expect more frames in the
} else if (frameOpcode == OPCODE_CONT) { // fragmented sequence
return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText); if (fragmentedFramesCount == 0) {
} else { // First text or binary frame for a fragmented set
throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode); fragmentedFramesText = null;
} if (frameOpcode == OPCODE_TEXT) {
case CORRUPT: checkUTF8String(channel, framePayload.array());
// If we don't keep reading Netty will throw an exception saying }
// we can't return null if no bytes read and state not changed. } else {
buffer.readByte(); // Subsequent frames - only check if init frame is text
return null; if (fragmentedFramesText != null) {
default: checkUTF8String(channel, framePayload.array());
throw new Error("Shouldn't reach here."); }
} }
}
private void unmask(ChannelBuffer frame) { // Increment counter
byte[] bytes = frame.array(); fragmentedFramesCount++;
for (int i = 0; i < bytes.length; i++) { }
frame.setByte(i, frame.getByte(i) ^ maskingKey.getByte(i % 4));
}
}
private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException { // Return the frame
checkpoint(State.CORRUPT); if (frameOpcode == OPCODE_TEXT) {
if (channel.isConnected()) { return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } else if (frameOpcode == OPCODE_BINARY) {
channel.close().awaitUninterruptibly(); return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
} } else if (frameOpcode == OPCODE_CONT) {
throw new CorruptedFrameException(reason); return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText);
} } else {
throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode);
}
case CORRUPT:
// If we don't keep reading Netty will throw an exception saying
// we can't return null if no bytes read and state not changed.
buffer.readByte();
return null;
default:
throw new Error("Shouldn't reach here.");
}
}
private int toFrameLength(long l) throws TooLongFrameException { private void unmask(ChannelBuffer frame) {
if (l > Integer.MAX_VALUE) { byte[] bytes = frame.array();
throw new TooLongFrameException("Length:" + l); for (int i = 0; i < bytes.length; i++) {
} else { frame.setByte(i, frame.getByte(i) ^ maskingKey.getByte(i % 4));
return (int) l; }
} }
}
private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException { private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException {
try { checkpoint(State.CORRUPT);
// StringBuilder sb = new StringBuilder("UTF8 " + bytes.length + if (channel.isConnected()) {
// " bytes: "); channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
// for (byte b : bytes) { channel.close().awaitUninterruptibly();
// sb.append(Integer.toHexString(b)).append(" "); }
// } throw new CorruptedFrameException(reason);
// logger.debug(sb.toString()); }
if (fragmentedFramesText == null) { private int toFrameLength(long l) throws TooLongFrameException {
fragmentedFramesText = new UTF8Output(bytes); if (l > Integer.MAX_VALUE) {
} else { throw new TooLongFrameException("Length:" + l);
fragmentedFramesText.write(bytes); } else {
} return (int) l;
} catch (UTF8Exception ex) { }
protocolViolation(channel, "invalid UTF-8 bytes"); }
}
} private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException {
try {
// StringBuilder sb = new StringBuilder("UTF8 " + bytes.length +
// " bytes: ");
// for (byte b : bytes) {
// sb.append(Integer.toHexString(b)).append(" ");
// }
// logger.debug(sb.toString());
if (fragmentedFramesText == null) {
fragmentedFramesText = new UTF8Output(bytes);
} else {
fragmentedFramesText.write(bytes);
}
} catch (UTF8Exception ex) {
protocolViolation(channel, "invalid UTF-8 bytes");
}
}
} }

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
} }

View File

@ -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);
}
}

View File

@ -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);
}
} }

View File

@ -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
} }