Added support for Hybi V17 and run against Autobahn V0.4.3
This commit is contained in:
parent
8efe131eb0
commit
48addae927
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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