From 69d5be4225085512aea5f9676d2f6ae105226c5c Mon Sep 17 00:00:00 2001 From: Jeff Pinner Date: Wed, 23 May 2012 08:52:50 -0700 Subject: [PATCH 1/3] SPDY: remove frame size limit in frame decoder --- .../codec/spdy/DefaultSpdyDataFrame.java | 11 - .../handler/codec/spdy/SpdyCodecUtil.java | 29 +- .../handler/codec/spdy/SpdyDataFrame.java | 10 - .../handler/codec/spdy/SpdyFrameCodec.java | 12 +- .../handler/codec/spdy/SpdyFrameDecoder.java | 794 ++++++++++++------ .../handler/codec/spdy/SpdyFrameEncoder.java | 19 +- .../handler/codec/spdy/SpdyHttpDecoder.java | 4 +- .../handler/codec/spdy/SpdyNoOpFrame.java | 1 + .../codec/spdy/SpdySessionHandler.java | 31 +- .../codec/spdy/SpdySessionHandlerTest.java | 26 +- 10 files changed, 608 insertions(+), 329 deletions(-) diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java index fc1c203a6c..d64d2e3846 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java @@ -26,7 +26,6 @@ public class DefaultSpdyDataFrame implements SpdyDataFrame { private int streamID; private boolean last; - private boolean compressed; private ChannelBuffer data = ChannelBuffers.EMPTY_BUFFER; /** @@ -58,14 +57,6 @@ public class DefaultSpdyDataFrame implements SpdyDataFrame { this.last = last; } - public boolean isCompressed() { - return compressed; - } - - public void setCompressed(boolean compressed) { - this.compressed = compressed; - } - public ChannelBuffer getData() { return data; } @@ -87,8 +78,6 @@ public class DefaultSpdyDataFrame implements SpdyDataFrame { buf.append(getClass().getSimpleName()); buf.append("(last: "); buf.append(isLast()); - buf.append("; compressed: "); - buf.append(isCompressed()); buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java index bafe9a660a..342fd36e51 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -28,8 +28,7 @@ final class SpdyCodecUtil { static final int SPDY_MAX_LENGTH = 0xFFFFFF; // Length is a 24-bit field - static final byte SPDY_DATA_FLAG_FIN = 0x01; - static final byte SPDY_DATA_FLAG_COMPRESS = 0x02; + static final byte SPDY_DATA_FLAG_FIN = 0x01; static final int SPDY_SYN_STREAM_FRAME = 1; static final int SPDY_SYN_REPLY_FRAME = 2; @@ -91,37 +90,37 @@ final class SpdyCodecUtil { * Reads a big-endian unsigned short integer from the buffer. */ static int getUnsignedShort(ChannelBuffer buf, int offset) { - return (int) ((buf.getByte(offset) & 0xFF) << 8 | - (buf.getByte(offset + 1) & 0xFF)); + return (buf.getByte(offset) & 0xFF) << 8 | + buf.getByte(offset + 1) & 0xFF; } /** * Reads a big-endian unsigned medium integer from the buffer. */ static int getUnsignedMedium(ChannelBuffer buf, int offset) { - return (int) ((buf.getByte(offset) & 0xFF) << 16 | - (buf.getByte(offset + 1) & 0xFF) << 8 | - (buf.getByte(offset + 2) & 0xFF)); + return (buf.getByte(offset) & 0xFF) << 16 | + (buf.getByte(offset + 1) & 0xFF) << 8 | + buf.getByte(offset + 2) & 0xFF; } /** * Reads a big-endian (31-bit) integer from the buffer. */ static int getUnsignedInt(ChannelBuffer buf, int offset) { - return (int) ((buf.getByte(offset) & 0x7F) << 24 | - (buf.getByte(offset + 1) & 0xFF) << 16 | - (buf.getByte(offset + 2) & 0xFF) << 8 | - (buf.getByte(offset + 3) & 0xFF)); + return (buf.getByte(offset) & 0x7F) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + buf.getByte(offset + 3) & 0xFF; } /** * Reads a big-endian signed integer from the buffer. */ static int getSignedInt(ChannelBuffer buf, int offset) { - return (int) ((buf.getByte(offset) & 0xFF) << 24 | - (buf.getByte(offset + 1) & 0xFF) << 16 | - (buf.getByte(offset + 2) & 0xFF) << 8 | - (buf.getByte(offset + 3) & 0xFF)); + return (buf.getByte(offset) & 0xFF) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + buf.getByte(offset + 3) & 0xFF; } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java index 27e4123e79..7f08a9aaf7 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java @@ -44,16 +44,6 @@ public interface SpdyDataFrame { */ void setLast(boolean last); - /** - * Returns {@code true} if the data in this frame has been compressed. - */ - boolean isCompressed(); - - /** - * Sets if the data in this frame has been compressed. - */ - void setCompressed(boolean compressed); - /** * Returns the data payload of this frame. If there is no data payload * {@link ChannelBuffers#EMPTY_BUFFER} is returned. diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java index 8fe63d66c2..35350bfafb 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java @@ -33,21 +33,21 @@ public class SpdyFrameCodec implements ChannelUpstreamHandler, /** * Creates a new instance with the default decoder and encoder options - * ({@code maxChunkSize (8192)}, {@code maxFrameSize (65536)}, - * {@code maxHeaderSize (16384)}, {@code compressionLevel (6)}, - * {@code windowBits (15)}, and {@code memLevel (8)}). + * ({@code maxChunkSize (8192)}, {@code maxHeaderSize (16384)}, + * {@code compressionLevel (6)}, {@code windowBits (15)}, and + * {@code memLevel (8)}). */ public SpdyFrameCodec() { - this(8192, 65536, 16384, 6, 15, 8); + this(8192, 16384, 6, 15, 8); } /** * Creates a new instance with the specified decoder and encoder options. */ public SpdyFrameCodec( - int maxChunkSize, int maxFrameSize, int maxHeaderSize, + int maxChunkSize, int maxHeaderSize, int compressionLevel, int windowBits, int memLevel) { - decoder = new SpdyFrameDecoder(maxChunkSize, maxFrameSize, maxHeaderSize); + decoder = new SpdyFrameDecoder(maxChunkSize, maxHeaderSize); encoder = new SpdyFrameEncoder(compressionLevel, windowBits, memLevel); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java index 0ee869b07c..d2a5a04657 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -15,13 +15,15 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + import io.netty.buffer.ChannelBuffer; import io.netty.buffer.ChannelBuffers; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.Channels; import io.netty.handler.codec.frame.FrameDecoder; - -import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import io.netty.handler.codec.frame.TooLongFrameException; /** * Decodes {@link ChannelBuffer}s into SPDY Data and Control Frames. @@ -29,41 +31,62 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; public class SpdyFrameDecoder extends FrameDecoder { private final int maxChunkSize; - private final int maxFrameSize; private final int maxHeaderSize; private final SpdyHeaderBlockDecompressor headerBlockDecompressor = SpdyHeaderBlockDecompressor.newInstance(); + private State state; + private SpdySettingsFrame spdySettingsFrame; + private SpdyHeaderBlock spdyHeaderBlock; + + // SPDY common header fields + private byte flags; + private int length; + private int version; + private int type; + private int streamID; + + // Header block decoding fields + private int headerSize; + private int numHeaders; + private ChannelBuffer decompressed; + + private static enum State { + READ_COMMON_HEADER, + READ_CONTROL_FRAME, + READ_SETTINGS_FRAME, + READ_HEADER_BLOCK_FRAME, + READ_HEADER_BLOCK, + READ_DATA_FRAME, + DISCARD_FRAME, + FRAME_ERROR + } + /** - * Creates a new instance with the default {@code maxChunkSize (8192)}, - * {@code maxFrameSize (65536)}, and {@code maxHeaderSize (16384)}. + * Creates a new instance with the default {@code maxChunkSize (8192)} + * and {@code maxHeaderSize (16384)}. */ public SpdyFrameDecoder() { - this(8192, 65536, 16384); + this(8192, 16384); } /** * Creates a new instance with the specified parameters. */ - public SpdyFrameDecoder( - int maxChunkSize, int maxFrameSize, int maxHeaderSize) { - super(true); // Enable unfold for data frames + public SpdyFrameDecoder(int maxChunkSize, int maxHeaderSize) { + super(false); if (maxChunkSize <= 0) { throw new IllegalArgumentException( "maxChunkSize must be a positive integer: " + maxChunkSize); } - if (maxFrameSize <= 0) { - throw new IllegalArgumentException( - "maxFrameSize must be a positive integer: " + maxFrameSize); - } if (maxHeaderSize <= 0) { throw new IllegalArgumentException( "maxHeaderSize must be a positive integer: " + maxHeaderSize); } this.maxChunkSize = maxChunkSize; - this.maxFrameSize = maxFrameSize; this.maxHeaderSize = maxHeaderSize; + state = State.READ_COMMON_HEADER; } @Override @@ -78,310 +101,470 @@ public class SpdyFrameDecoder extends FrameDecoder { } } - @Override protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception { - - // Must read common header to determine frame length - if (buffer.readableBytes() < SPDY_HEADER_SIZE) { - return null; - } - - // Get frame length from common header - int frameOffset = buffer.readerIndex(); - int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; - int dataLength = getUnsignedMedium(buffer, lengthOffset); - int frameLength = SPDY_HEADER_SIZE + dataLength; - - // Throw exception if frameLength exceeds maxFrameSize - if (frameLength > maxFrameSize) { - throw new SpdyProtocolException( - "Frame length exceeds " + maxFrameSize + ": " + frameLength); - } - - // Wait until entire frame is readable - if (buffer.readableBytes() < frameLength) { - return null; - } - - // Read common header fields - boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; - int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; - byte flags = buffer.getByte(flagsOffset); - - if (control) { - // Decode control frame common header - int version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; - - // Spdy versioning spec is broken - if (version != SPDY_VERSION) { - buffer.skipBytes(frameLength); - throw new SpdyProtocolException( - "Unsupported version: " + version); - } - - int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; - int type = getUnsignedShort(buffer, typeOffset); - buffer.skipBytes(SPDY_HEADER_SIZE); - - int readerIndex = buffer.readerIndex(); - buffer.skipBytes(dataLength); - return decodeControlFrame(type, flags, buffer.slice(readerIndex, dataLength)); - } else { - // Decode data frame common header - int streamID = getUnsignedInt(buffer, frameOffset); - buffer.skipBytes(SPDY_HEADER_SIZE); - - // Generate data frames that do not exceed maxChunkSize - int numFrames = dataLength / maxChunkSize; - if (dataLength % maxChunkSize != 0) { - numFrames ++; - } - SpdyDataFrame[] frames = new SpdyDataFrame[numFrames]; - for (int i = 0; i < numFrames; i++) { - int chunkSize = Math.min(maxChunkSize, dataLength); - SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); - spdyDataFrame.setCompressed((flags & SPDY_DATA_FLAG_COMPRESS) != 0); - spdyDataFrame.setData(buffer.readBytes(chunkSize)); - dataLength -= chunkSize; - if (dataLength == 0) { - spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + switch(state) { + case READ_COMMON_HEADER: + state = readCommonHeader(buffer); + if (state == State.FRAME_ERROR) { + if (version != SPDY_VERSION) { + fireProtocolException(ctx, "Unsupported version: " + version); + } else { + fireInvalidControlFrameException(ctx); } - frames[i] = spdyDataFrame; + } + return null; + + case READ_CONTROL_FRAME: + try { + Object frame = readControlFrame(buffer); + if (frame != null) { + state = State.READ_COMMON_HEADER; + } + return frame; + } catch (IllegalArgumentException e) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + } + return null; + + case READ_SETTINGS_FRAME: + if (spdySettingsFrame == null) { + // Validate frame length against number of entries + if (buffer.readableBytes() < 4) { + return null; + } + int numEntries = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // Each ID/Value entry is 8 bytes + if ((length & 0x07) != 0 || length >> 3 != numEntries) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + return null; + } + + spdySettingsFrame = new DefaultSpdySettingsFrame(); + + boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; + spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); } - return frames; - } - } - - private Object decodeControlFrame(int type, byte flags, ChannelBuffer data) - throws Exception { - int streamID; - boolean last; - - switch (type) { - case SPDY_SYN_STREAM_FRAME: - if (data.readableBytes() < 12) { - throw new SpdyProtocolException( - "Received invalid SYN_STREAM control frame"); - } - streamID = getUnsignedInt(data, data.readerIndex()); - int associatedToStreamID = getUnsignedInt(data, data.readerIndex() + 4); - byte priority = (byte) (data.getByte(data.readerIndex() + 8) >> 6 & 0x03); - data.skipBytes(10); - - SpdySynStreamFrame spdySynStreamFrame = - new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); - - last = (flags & SPDY_FLAG_FIN) != 0; - boolean unid = (flags & SPDY_FLAG_UNIDIRECTIONAL) != 0; - spdySynStreamFrame.setLast(last); - spdySynStreamFrame.setUnidirectional(unid); - - decodeHeaderBlock(spdySynStreamFrame, data); - - return spdySynStreamFrame; - - case SPDY_SYN_REPLY_FRAME: - if (data.readableBytes() < 8) { - throw new SpdyProtocolException( - "Received invalid SYN_REPLY control frame"); - } - streamID = getUnsignedInt(data, data.readerIndex()); - data.skipBytes(6); - - SpdySynReplyFrame spdySynReplyFrame = - new DefaultSpdySynReplyFrame(streamID); - - last = (flags & SPDY_FLAG_FIN) != 0; - spdySynReplyFrame.setLast(last); - - decodeHeaderBlock(spdySynReplyFrame, data); - - return spdySynReplyFrame; - - case SPDY_RST_STREAM_FRAME: - if (flags != 0 || data.readableBytes() != 8) { - throw new SpdyProtocolException( - "Received invalid RST_STREAM control frame"); - } - streamID = getUnsignedInt(data, data.readerIndex()); - int statusCode = getSignedInt(data, data.readerIndex() + 4); - if (statusCode == 0) { - throw new SpdyProtocolException( - "Received invalid RST_STREAM status code"); - } - - return new DefaultSpdyRstStreamFrame(streamID, statusCode); - - case SPDY_SETTINGS_FRAME: - if (data.readableBytes() < 4) { - throw new SpdyProtocolException( - "Received invalid SETTINGS control frame"); - } - // Each ID/Value entry is 8 bytes - // The number of entries cannot exceed SPDY_MAX_LENGTH / 8; - int numEntries = getUnsignedInt(data, data.readerIndex()); - if ((numEntries > (SPDY_MAX_LENGTH - 4) / 8) || - (data.readableBytes() != numEntries * 8 + 4)) { - throw new SpdyProtocolException( - "Received invalid SETTINGS control frame"); - } - data.skipBytes(4); - - SpdySettingsFrame spdySettingsFrame = new DefaultSpdySettingsFrame(); - - boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; - spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); - - for (int i = 0; i < numEntries; i ++) { + int readableEntries = Math.min(buffer.readableBytes() >> 3, length >> 3); + for (int i = 0; i < readableEntries; i ++) { // Chromium Issue 79156 // SPDY setting ids are not written in network byte order // Read id assuming the architecture is little endian - int ID = (data.readByte() & 0xFF) | - (data.readByte() & 0xFF) << 8 | - (data.readByte() & 0xFF) << 16; - byte ID_flags = data.readByte(); - int value = getSignedInt(data, data.readerIndex()); - data.skipBytes(4); + int ID = buffer.readByte() & 0xFF | + (buffer.readByte() & 0xFF) << 8 | + (buffer.readByte() & 0xFF) << 16; + byte ID_flags = buffer.readByte(); + int value = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); - if (!(spdySettingsFrame.isSet(ID))) { + // Check for invalid ID -- avoid IllegalArgumentException in setValue + if (ID == 0) { + state = State.FRAME_ERROR; + spdySettingsFrame = null; + fireInvalidControlFrameException(ctx); + return null; + } + + if (!spdySettingsFrame.isSet(ID)) { boolean persistVal = (ID_flags & SPDY_SETTINGS_PERSIST_VALUE) != 0; boolean persisted = (ID_flags & SPDY_SETTINGS_PERSISTED) != 0; spdySettingsFrame.setValue(ID, value, persistVal, persisted); } } - return spdySettingsFrame; - - case SPDY_NOOP_FRAME: - if (data.readableBytes() != 0) { - throw new SpdyProtocolException( - "Received invalid NOOP control frame"); + length -= 8 * readableEntries; + if (length == 0) { + state = State.READ_COMMON_HEADER; + Object frame = spdySettingsFrame; + spdySettingsFrame = null; + return frame; } - return null; - case SPDY_PING_FRAME: - if (data.readableBytes() != 4) { - throw new SpdyProtocolException( - "Received invalid PING control frame"); + case READ_HEADER_BLOCK_FRAME: + try { + spdyHeaderBlock = readHeaderBlockFrame(buffer); + if (spdyHeaderBlock != null) { + if (length == 0) { + state = State.READ_COMMON_HEADER; + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + return frame; + } + state = State.READ_HEADER_BLOCK; + } + return null; + } catch (IllegalArgumentException e) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + return null; } - int ID = getSignedInt(data, data.readerIndex()); + + case READ_HEADER_BLOCK: + int compressedBytes = Math.min(buffer.readableBytes(), length); + length -= compressedBytes; + + try { + decodeHeaderBlock(buffer.readSlice(compressedBytes)); + } catch (Exception e) { + state = State.FRAME_ERROR; + spdyHeaderBlock = null; + decompressed = null; + Channels.fireExceptionCaught(ctx, e); + return null; + } + + if (spdyHeaderBlock != null && spdyHeaderBlock.isInvalid()) { + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + decompressed = null; + if (length == 0) { + state = State.READ_COMMON_HEADER; + } + return frame; + } + + if (length == 0) { + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + state = State.READ_COMMON_HEADER; + return frame; + } + return null; + + case READ_DATA_FRAME: + if (streamID == 0) { + state = State.FRAME_ERROR; + fireProtocolException(ctx, "Received invalid data frame"); + return null; + } + + // Generate data frames that do not exceed maxChunkSize + int dataLength = Math.min(maxChunkSize, length); + + // Wait until entire frame is readable + if (buffer.readableBytes() < dataLength) { + return null; + } + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setData(buffer.readBytes(dataLength)); + length -= dataLength; + + if (length == 0) { + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + state = State.READ_COMMON_HEADER; + } + return spdyDataFrame; + + case DISCARD_FRAME: + int numBytes = Math.min(buffer.readableBytes(), length); + buffer.skipBytes(numBytes); + length -= numBytes; + if (length == 0) { + state = State.READ_COMMON_HEADER; + } + return null; + + case FRAME_ERROR: + buffer.skipBytes(buffer.readableBytes()); + return null; + + default: + throw new Error("Shouldn't reach here."); + } + } + + private State readCommonHeader(ChannelBuffer buffer) { + // Wait until entire header is readable + if (buffer.readableBytes() < SPDY_HEADER_SIZE) { + return State.READ_COMMON_HEADER; + } + + int frameOffset = buffer.readerIndex(); + int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; + int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; + buffer.skipBytes(SPDY_HEADER_SIZE); + + // Read common header fields + boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; + flags = buffer.getByte(flagsOffset); + length = getUnsignedMedium(buffer, lengthOffset); + + if (control) { + // Decode control frame common header + version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; + + int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; + type = getUnsignedShort(buffer, typeOffset); + + // Check version first then validity + if (version != SPDY_VERSION || !isValidControlFrameHeader()) { + return State.FRAME_ERROR; + } + + // Make sure decoder will produce a frame or consume input + State nextState; + if (willGenerateControlFrame()) { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + case SPDY_SYN_REPLY_FRAME: + case SPDY_HEADERS_FRAME: + nextState = State.READ_HEADER_BLOCK_FRAME; + break; + + case SPDY_SETTINGS_FRAME: + nextState = State.READ_SETTINGS_FRAME; + break; + + default: + nextState = State.READ_CONTROL_FRAME; + } + } else if (length != 0) { + nextState = State.DISCARD_FRAME; + } else { + nextState = State.READ_COMMON_HEADER; + } + return nextState; + } else { + // Decode data frame common header + streamID = getUnsignedInt(buffer, frameOffset); + + return State.READ_DATA_FRAME; + } + } + + private Object readControlFrame(ChannelBuffer buffer) { + switch (type) { + case SPDY_RST_STREAM_FRAME: + if (buffer.readableBytes() < 8) { + return null; + } + + int streamID = getUnsignedInt(buffer, buffer.readerIndex()); + int statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + return new DefaultSpdyRstStreamFrame(streamID, statusCode); + + case SPDY_PING_FRAME: + if (buffer.readableBytes() < 4) { + return null; + } + + int ID = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); return new DefaultSpdyPingFrame(ID); case SPDY_GOAWAY_FRAME: - if (data.readableBytes() != 4) { - throw new SpdyProtocolException( - "Received invalid GOAWAY control frame"); + if (buffer.readableBytes() < 4) { + return null; } - int lastGoodStreamID = getUnsignedInt(data, data.readerIndex()); + + int lastGoodStreamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); return new DefaultSpdyGoAwayFrame(lastGoodStreamID); - case SPDY_HEADERS_FRAME: - // Protocol allows length 4 frame when there are no name/value pairs - if (data.readableBytes() == 4) { - streamID = getUnsignedInt(data, data.readerIndex()); - return new DefaultSpdyHeadersFrame(streamID); - } - - if (data.readableBytes() < 8) { - throw new SpdyProtocolException( - "Received invalid HEADERS control frame"); - } - streamID = getUnsignedInt(data, data.readerIndex()); - data.skipBytes(6); - - SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamID); - - decodeHeaderBlock(spdyHeadersFrame, data); - - return spdyHeadersFrame; - - case SPDY_WINDOW_UPDATE_FRAME: - return null; - default: - return null; + throw new Error("Shouldn't reach here."); } } - private boolean ensureBytes(ChannelBuffer decompressed, int bytes) throws Exception { + private SpdyHeaderBlock readHeaderBlockFrame(ChannelBuffer buffer) { + int streamID; + switch (type) { + case SPDY_SYN_STREAM_FRAME: + if (buffer.readableBytes() < 12) { + return null; + } + + int offset = buffer.readerIndex(); + streamID = getUnsignedInt(buffer, offset); + int associatedToStreamID = getUnsignedInt(buffer, offset + 4); + byte priority = (byte) (buffer.getByte(offset + 8) >> 6 & 0x03); + buffer.skipBytes(10); + length -= 10; + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdySynStreamFrame spdySynStreamFrame = + new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); + spdySynStreamFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + spdySynStreamFrame.setUnidirectional((flags & SPDY_FLAG_UNIDIRECTIONAL) != 0); + + return spdySynStreamFrame; + + case SPDY_SYN_REPLY_FRAME: + if (buffer.readableBytes() < 8) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(6); + length -= 6; + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + spdySynReplyFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + + return spdySynReplyFrame; + + case SPDY_HEADERS_FRAME: + // Protocol allows length 4 frame when there are no name/value pairs + int minLength = length == 4 ? 4 : 8; + if (buffer.readableBytes() < minLength) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (length == 4 && buffer.getShort(buffer.readerIndex() + 2) == 0) { + buffer.skipBytes(4); + length = 0; + } else if (length != 0) { + buffer.skipBytes(2); + length -= 2; + } + + return new DefaultSpdyHeadersFrame(streamID); + + default: + throw new Error("Shouldn't reach here."); + } + } + + private boolean ensureBytes(int bytes) throws Exception { if (decompressed.readableBytes() >= bytes) { return true; } - decompressed.discardReadBytes(); + // Perhaps last call to decode filled output buffer headerBlockDecompressor.decode(decompressed); return decompressed.readableBytes() >= bytes; } - private void decodeHeaderBlock(SpdyHeaderBlock headerFrame, ChannelBuffer headerBlock) - throws Exception { - if ((headerBlock.readableBytes() == 2) && - (headerBlock.getShort(headerBlock.readerIndex()) == 0)) { + private int readLengthField() { + return decompressed.readUnsignedShort(); + } + + private void decodeHeaderBlock(ChannelBuffer buffer) throws Exception { + if (decompressed == null) { + // First time we start to decode a header block + // Initialize header block decoding fields + headerSize = 0; + numHeaders = -1; + decompressed = ChannelBuffers.dynamicBuffer(8192); + } + + // Accumulate decompressed data + headerBlockDecompressor.setInput(buffer); + headerBlockDecompressor.decode(decompressed); + + if (spdyHeaderBlock == null) { + // Only decompressing data to keep decompression context in sync + decompressed = null; return; } - headerBlockDecompressor.setInput(headerBlock); - ChannelBuffer decompressed = ChannelBuffers.dynamicBuffer(8192); - headerBlockDecompressor.decode(decompressed); + int lengthFieldSize = 2; // SPDY/2 uses 16-bit length fields - if (decompressed.readableBytes() < 2) { - throw new SpdyProtocolException( - "Received invalid header block"); - } - int headerSize = 0; - int numEntries = decompressed.readUnsignedShort(); - for (int i = 0; i < numEntries; i ++) { - if (!ensureBytes(decompressed, 2)) { - throw new SpdyProtocolException( - "Received invalid header block"); + if (numHeaders == -1) { + // Read number of Name/Value pairs + if (decompressed.readableBytes() < lengthFieldSize) { + return; } - int nameLength = decompressed.readUnsignedShort(); + numHeaders = readLengthField(); + } + + while (numHeaders > 0) { + int headerSize = this.headerSize; + decompressed.markReaderIndex(); + + // Try to read length of name + if (!ensureBytes(lengthFieldSize)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; + } + int nameLength = readLengthField(); + + // Recipients of a zero-length name must issue a stream error if (nameLength == 0) { - headerFrame.setInvalid(); + spdyHeaderBlock.setInvalid(); return; } headerSize += nameLength; if (headerSize > maxHeaderSize) { - throw new SpdyProtocolException( + throw new TooLongFrameException( "Header block exceeds " + maxHeaderSize); } - if (!ensureBytes(decompressed, nameLength)) { - throw new SpdyProtocolException( - "Received invalid header block"); + + // Try to read name + if (!ensureBytes(nameLength)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; } byte[] nameBytes = new byte[nameLength]; decompressed.readBytes(nameBytes); String name = new String(nameBytes, "UTF-8"); - if (headerFrame.containsHeader(name)) { - throw new SpdyProtocolException( - "Received duplicate header name: " + name); + + // Check for identically named headers + if (spdyHeaderBlock.containsHeader(name)) { + spdyHeaderBlock.setInvalid(); + return; } - if (!ensureBytes(decompressed, 2)) { - throw new SpdyProtocolException( - "Received invalid header block"); + + // Try to read length of value + if (!ensureBytes(lengthFieldSize)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; } - int valueLength = decompressed.readUnsignedShort(); + int valueLength = readLengthField(); + + // Recipients of illegal value fields must issue a stream error if (valueLength == 0) { - headerFrame.setInvalid(); + spdyHeaderBlock.setInvalid(); return; } headerSize += valueLength; if (headerSize > maxHeaderSize) { - throw new SpdyProtocolException( + throw new TooLongFrameException( "Header block exceeds " + maxHeaderSize); } - if (!ensureBytes(decompressed, valueLength)) { - throw new SpdyProtocolException( - "Received invalid header block"); + + // Try to read value + if (!ensureBytes(valueLength)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; } byte[] valueBytes = new byte[valueLength]; decompressed.readBytes(valueBytes); + + // Add Name/Value pair to headers int index = 0; int offset = 0; while (index < valueLength) { @@ -390,14 +573,121 @@ public class SpdyFrameDecoder extends FrameDecoder { } if (index < valueBytes.length && valueBytes[index + 1] == (byte) 0) { // Received multiple, in-sequence NULL characters - headerFrame.setInvalid(); + // Recipients of illegal value fields must issue a stream error + spdyHeaderBlock.setInvalid(); return; } String value = new String(valueBytes, offset, index - offset, "UTF-8"); - headerFrame.addHeader(name, value); + + try { + spdyHeaderBlock.addHeader(name, value); + } catch (IllegalArgumentException e) { + // Name contains NULL or non-ascii characters + spdyHeaderBlock.setInvalid(); + return; + } index ++; offset = index; } + numHeaders --; + this.headerSize = headerSize; + } + decompressed = null; + } + + private boolean isValidControlFrameHeader() { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + return length >= 12; + + case SPDY_SYN_REPLY_FRAME: + return length >= 8; + + case SPDY_RST_STREAM_FRAME: + return flags == 0 && length == 8; + + case SPDY_SETTINGS_FRAME: + return length >= 4; + + case SPDY_NOOP_FRAME: + return length == 0; + + case SPDY_PING_FRAME: + return length == 4; + + case SPDY_GOAWAY_FRAME: + return length == 4; + + case SPDY_HEADERS_FRAME: + return length == 4 || length >= 8; + + case SPDY_WINDOW_UPDATE_FRAME: + default: + return true; } } + + private boolean willGenerateControlFrame() { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + case SPDY_SYN_REPLY_FRAME: + case SPDY_RST_STREAM_FRAME: + case SPDY_SETTINGS_FRAME: + case SPDY_PING_FRAME: + case SPDY_GOAWAY_FRAME: + case SPDY_HEADERS_FRAME: + return true; + + case SPDY_NOOP_FRAME: + case SPDY_WINDOW_UPDATE_FRAME: + default: + return false; + } + } + + private void fireInvalidControlFrameException(ChannelHandlerContext ctx) { + String message = "Received invalid control frame"; + switch (type) { + case SPDY_SYN_STREAM_FRAME: + message = "Received invalid SYN_STREAM control frame"; + break; + + case SPDY_SYN_REPLY_FRAME: + message = "Received invalid SYN_REPLY control frame"; + break; + + case SPDY_RST_STREAM_FRAME: + message = "Received invalid RST_STREAM control frame"; + break; + + case SPDY_SETTINGS_FRAME: + message = "Received invalid SETTINGS control frame"; + break; + + case SPDY_NOOP_FRAME: + message = "Received invalid NOOP control frame"; + break; + + case SPDY_PING_FRAME: + message = "Received invalid PING control frame"; + break; + + case SPDY_GOAWAY_FRAME: + message = "Received invalid GOAWAY control frame"; + break; + + case SPDY_HEADERS_FRAME: + message = "Received invalid HEADERS control frame"; + break; + + case SPDY_WINDOW_UPDATE_FRAME: + message = "Received invalid WINDOW_UPDATE control frame"; + break; + } + fireProtocolException(ctx, message); + } + + private static void fireProtocolException(ChannelHandlerContext ctx, String message) { + Channels.fireExceptionCaught(ctx, new SpdyProtocolException(message)); + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java index 878dacf405..39619dfbb4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -82,9 +82,6 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; ChannelBuffer data = spdyDataFrame.getData(); byte flags = spdyDataFrame.isLast() ? SPDY_DATA_FLAG_FIN : 0; - if (spdyDataFrame.isCompressed()) { - flags |= SPDY_DATA_FLAG_COMPRESS; - } ChannelBuffer header = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); header.writeInt(spdyDataFrame.getStreamID() & 0x7FFFFFFF); @@ -102,7 +99,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { flags |= SPDY_FLAG_UNIDIRECTIONAL; } int headerBlockLength = data.readableBytes(); - int length = (headerBlockLength == 0) ? 12 : 10 + headerBlockLength; + int length = headerBlockLength == 0 ? 12 : 10 + headerBlockLength; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); frame.writeShort(SPDY_VERSION | 0x8000); @@ -111,7 +108,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { frame.writeMedium(length); frame.writeInt(spdySynStreamFrame.getStreamID()); frame.writeInt(spdySynStreamFrame.getAssociatedToStreamID()); - frame.writeShort(((short) spdySynStreamFrame.getPriority()) << 14); + frame.writeShort((spdySynStreamFrame.getPriority() & 0xFF) << 14); if (data.readableBytes() == 0) { frame.writeShort(0); } @@ -124,7 +121,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { encodeHeaderBlock(spdySynReplyFrame)); byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; int headerBlockLength = data.readableBytes(); - int length = (headerBlockLength == 0) ? 8 : 6 + headerBlockLength; + int length = headerBlockLength == 0 ? 8 : 6 + headerBlockLength; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); frame.writeShort(SPDY_VERSION | 0x8000); @@ -178,9 +175,9 @@ public class SpdyFrameEncoder extends OneToOneEncoder { // Chromium Issue 79156 // SPDY setting ids are not written in network byte order // Write id assuming the architecture is little endian - frame.writeByte((id >> 0) & 0xFF); - frame.writeByte((id >> 8) & 0xFF); - frame.writeByte((id >> 16) & 0xFF); + frame.writeByte(id >> 0 & 0xFF); + frame.writeByte(id >> 8 & 0xFF); + frame.writeByte(id >> 16 & 0xFF); frame.writeByte(ID_flags); frame.writeInt(spdySettingsFrame.getValue(id)); } @@ -223,7 +220,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { ChannelBuffer data = compressHeaderBlock( encodeHeaderBlock(spdyHeadersFrame)); int headerBlockLength = data.readableBytes(); - int length = (headerBlockLength == 0) ? 4 : 6 + headerBlockLength; + int length = headerBlockLength == 0 ? 4 : 6 + headerBlockLength; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); frame.writeShort(SPDY_VERSION | 0x8000); @@ -240,7 +237,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { return msg; } - private ChannelBuffer encodeHeaderBlock(SpdyHeaderBlock headerFrame) + private static ChannelBuffer encodeHeaderBlock(SpdyHeaderBlock headerFrame) throws Exception { Set names = headerFrame.getHeaderNames(); int numHeaders = names.size(); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java index a16ab9fccd..0a6245bc9a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java @@ -220,7 +220,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { return null; } - private HttpRequest createHttpRequest(SpdyHeaderBlock requestFrame) + private static HttpRequest createHttpRequest(SpdyHeaderBlock requestFrame) throws Exception { // Create the first line of the request from the name/value pairs HttpMethod method = SpdyHeaders.getMethod(requestFrame); @@ -250,7 +250,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { return httpRequest; } - private HttpResponse createHttpResponse(SpdyHeaderBlock responseFrame) + private static HttpResponse createHttpResponse(SpdyHeaderBlock responseFrame) throws Exception { // Create the first line of the response from the name/value pairs HttpResponseStatus status = SpdyHeaders.getStatus(responseFrame); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java index 8b955ecd27..f923051d00 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java @@ -19,4 +19,5 @@ package io.netty.handler.codec.spdy; * A SPDY Protocol NOOP Control Frame */ public interface SpdyNoOpFrame { + // Tag interface } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java index c2a77220d9..b89d136cc5 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java @@ -27,13 +27,14 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelStateEvent; import io.netty.channel.Channels; +import io.netty.channel.ExceptionEvent; import io.netty.channel.MessageEvent; import io.netty.channel.SimpleChannelUpstreamHandler; /** * Manages streams within a SPDY session. */ -public class SpdySessionHandler extends SimpleChannelUpstreamHandler +public class SpdySessionHandler extends SimpleChannelUpstreamHandler implements ChannelDownstreamHandler { private static final SpdyProtocolException PROTOCOL_EXCEPTION = new SpdyProtocolException(); @@ -217,12 +218,12 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler */ SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; - + if (isRemoteInitiatedID(spdyPingFrame.getID())) { Channels.write(ctx, Channels.future(e.getChannel()), spdyPingFrame, e.getRemoteAddress()); return; } - + // Note: only checks that there are outstanding pings since uniqueness is not inforced if (pings.get() == 0) { return; @@ -253,6 +254,18 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler super.messageReceived(ctx, e); } + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) + throws Exception { + + Throwable cause = e.getCause(); + if (cause instanceof SpdyProtocolException) { + issueSessionError(ctx, e.getChannel(), null); + } + + super.exceptionCaught(ctx, e); + } + public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) throws Exception { if (evt instanceof ChannelStateEvent) { @@ -380,7 +393,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler private boolean isRemoteInitiatedID(int ID) { boolean serverID = SpdyCodecUtil.isServerID(ID); - return (server && !serverID) || (!server && serverID); + return server && !serverID || !server && serverID; } private synchronized void updateConcurrentStreams(SpdySettingsFrame settings, boolean remote) { @@ -416,8 +429,8 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler if (receivedGoAwayFrame || sentGoAwayFrame) { return false; } - if ((maxConcurrentStreams != 0) && - (spdySession.numActiveStreams() >= maxConcurrentStreams)) { + if (maxConcurrentStreams != 0 && + spdySession.numActiveStreams() >= maxConcurrentStreams) { return false; } spdySession.acceptStream(streamID, remoteSideClosed, localSideClosed); @@ -433,14 +446,14 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } else { spdySession.closeLocalSide(streamID); } - if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + if (closeSessionFuture != null && spdySession.noActiveStreams()) { closeSessionFuture.setSuccess(); } } private void removeStream(int streamID) { spdySession.removeStream(streamID); - if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + if (closeSessionFuture != null && spdySession.noActiveStreams()) { closeSessionFuture.setSuccess(); } } @@ -466,7 +479,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler if (!sentGoAwayFrame) { sentGoAwayFrame = true; ChannelFuture future = Channels.future(channel); - Channels.write(ctx, future, new DefaultSpdyGoAwayFrame(lastGoodStreamID)); + Channels.write(ctx, future, new DefaultSpdyGoAwayFrame(lastGoodStreamID), remoteAddress); return future; } return Channels.succeededFuture(channel); diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java index 83bfdb6f7b..893f0baed1 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java @@ -18,9 +18,9 @@ package io.netty.handler.codec.spdy; import java.util.List; import java.util.Map; -import io.netty.channel.Channels; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelStateEvent; +import io.netty.channel.Channels; import io.netty.channel.MessageEvent; import io.netty.channel.SimpleChannelUpstreamHandler; import io.netty.handler.codec.embedder.DecoderEmbedder; @@ -36,7 +36,7 @@ public class SpdySessionHandlerTest { closeMessage.setValue(closeSignal, 0); } - private void assertHeaderBlock(SpdyHeaderBlock received, SpdyHeaderBlock expected) { + private static void assertHeaderBlock(SpdyHeaderBlock received, SpdyHeaderBlock expected) { for (String name: expected.getHeaderNames()) { List expectedValues = expected.getHeaders(name); List receivedValues = received.getHeaders(name); @@ -48,7 +48,7 @@ public class SpdySessionHandlerTest { Assert.assertTrue(received.getHeaders().isEmpty()); } - private void assertDataFrame(Object msg, int streamID, boolean last) { + private static void assertDataFrame(Object msg, int streamID, boolean last) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdyDataFrame); SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; @@ -56,7 +56,7 @@ public class SpdySessionHandlerTest { Assert.assertTrue(spdyDataFrame.isLast() == last); } - private void assertSynReply(Object msg, int streamID, boolean last, SpdyHeaderBlock headers) { + private static void assertSynReply(Object msg, int streamID, boolean last, SpdyHeaderBlock headers) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdySynReplyFrame); SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; @@ -65,7 +65,7 @@ public class SpdySessionHandlerTest { assertHeaderBlock(spdySynReplyFrame, headers); } - private void assertRstStream(Object msg, int streamID, SpdyStreamStatus status) { + private static void assertRstStream(Object msg, int streamID, SpdyStreamStatus status) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdyRstStreamFrame); SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; @@ -73,21 +73,21 @@ public class SpdySessionHandlerTest { Assert.assertTrue(spdyRstStreamFrame.getStatus().equals(status)); } - private void assertPing(Object msg, int ID) { + private static void assertPing(Object msg, int ID) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdyPingFrame); SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; Assert.assertTrue(spdyPingFrame.getID() == ID); } - private void assertGoAway(Object msg, int lastGoodStreamID) { + private static void assertGoAway(Object msg, int lastGoodStreamID) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdyGoAwayFrame); SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; Assert.assertTrue(spdyGoAwayFrame.getLastGoodStreamID() == lastGoodStreamID); } - private void assertHeaders(Object msg, int streamID, SpdyHeaderBlock headers) { + private static void assertHeaders(Object msg, int streamID, SpdyHeaderBlock headers) { Assert.assertNotNull(msg); Assert.assertTrue(msg instanceof SpdyHeadersFrame); SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; @@ -263,8 +263,8 @@ public class SpdySessionHandlerTest { // Echo Handler opens 4 half-closed streams on session connection // and then sets the number of concurrent streams to 3 private class EchoHandler extends SimpleChannelUpstreamHandler { - private int closeSignal; - private boolean server; + private final int closeSignal; + private final boolean server; EchoHandler(int closeSignal, boolean server) { super(); @@ -299,9 +299,9 @@ public class SpdySessionHandlerTest { public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Object msg = e.getMessage(); - if ((msg instanceof SpdyDataFrame) || - (msg instanceof SpdyPingFrame) || - (msg instanceof SpdyHeadersFrame)) { + if (msg instanceof SpdyDataFrame || + msg instanceof SpdyPingFrame || + msg instanceof SpdyHeadersFrame) { Channels.write(e.getChannel(), msg, e.getRemoteAddress()); return; From f60997686db58ab7623ddb15839d16169c59a702 Mon Sep 17 00:00:00 2001 From: Jeff Pinner Date: Wed, 23 May 2012 08:53:29 -0700 Subject: [PATCH 2/3] SPDY: fix for mozilla firefox bug 754766 --- .../spdy/DefaultSpdyWindowUpdateFrame.java | 76 +++++++++++++++++++ .../handler/codec/spdy/SpdyFrameDecoder.java | 38 +++++++++- .../handler/codec/spdy/SpdyFrameEncoder.java | 12 +++ .../codec/spdy/SpdyWindowUpdateFrame.java | 43 +++++++++++ .../spdy/AbstractSocketSpdyEchoTest.java | 20 +++-- 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java new file mode 100644 index 0000000000..5093213d6c --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyWindowUpdateFrame} implementation. + */ +public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame { + + private int streamID; + private int deltaWindowSize; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param deltaWindowSize the Delta-Window-Size of this frame + */ + public DefaultSpdyWindowUpdateFrame(int streamID, int deltaWindowSize) { + setStreamID(streamID); + setDeltaWindowSize(deltaWindowSize); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public int getDeltaWindowSize() { + return deltaWindowSize; + } + + public void setDeltaWindowSize(int deltaWindowSize) { + if (deltaWindowSize <= 0) { + throw new IllegalArgumentException( + "Delta-Window-Size must be positive: " + + deltaWindowSize); + } + this.deltaWindowSize = deltaWindowSize; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Delta-Window-Size = "); + buf.append(deltaWindowSize); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java index d2a5a04657..016aac0cc4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -115,6 +115,26 @@ public class SpdyFrameDecoder extends FrameDecoder { fireInvalidControlFrameException(ctx); } } + + // FrameDecoders must consume data when producing frames + // All length 0 frames must be generated now + if (length == 0) { + if (state == State.READ_DATA_FRAME) { + if (streamID == 0) { + state = State.FRAME_ERROR; + fireProtocolException(ctx, "Received invalid data frame"); + return null; + } + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + state = State.READ_COMMON_HEADER; + return spdyDataFrame; + } + // There are no length 0 control frames + state = State.READ_COMMON_HEADER; + } + return null; case READ_CONTROL_FRAME: @@ -343,13 +363,14 @@ public class SpdyFrameDecoder extends FrameDecoder { } private Object readControlFrame(ChannelBuffer buffer) { + int streamID; switch (type) { case SPDY_RST_STREAM_FRAME: if (buffer.readableBytes() < 8) { return null; } - int streamID = getUnsignedInt(buffer, buffer.readerIndex()); + streamID = getUnsignedInt(buffer, buffer.readerIndex()); int statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); buffer.skipBytes(8); @@ -375,6 +396,17 @@ public class SpdyFrameDecoder extends FrameDecoder { return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + case SPDY_WINDOW_UPDATE_FRAME: + if (buffer.readableBytes() < 8) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + int deltaWindowSize = getUnsignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + return new DefaultSpdyWindowUpdateFrame(streamID, deltaWindowSize); + default: throw new Error("Shouldn't reach here."); } @@ -622,6 +654,8 @@ public class SpdyFrameDecoder extends FrameDecoder { return length == 4 || length >= 8; case SPDY_WINDOW_UPDATE_FRAME: + return length == 8; + default: return true; } @@ -636,10 +670,10 @@ public class SpdyFrameDecoder extends FrameDecoder { case SPDY_PING_FRAME: case SPDY_GOAWAY_FRAME: case SPDY_HEADERS_FRAME: + case SPDY_WINDOW_UPDATE_FRAME: return true; case SPDY_NOOP_FRAME: - case SPDY_WINDOW_UPDATE_FRAME: default: return false; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java index 39619dfbb4..309c3551e3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -231,6 +231,18 @@ public class SpdyFrameEncoder extends OneToOneEncoder { frame.writeShort(0); } return ChannelBuffers.wrappedBuffer(frame, data); + + } else if (msg instanceof SpdyWindowUpdateFrame) { + + SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 8); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_WINDOW_UPDATE_FRAME); + frame.writeInt(8); + frame.writeInt(spdyWindowUpdateFrame.getStreamID()); + frame.writeInt(spdyWindowUpdateFrame.getDeltaWindowSize()); + return frame; } // Unknown message type diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java new file mode 100644 index 0000000000..18f9712987 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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 io.handler.codec.spdy; + +/** + * A SPDY Protocol WINDOW_UPDATE Control Frame + */ +public interface SpdyWindowUpdateFrame { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns the Delta-Window-Size of this frame. + */ + int getDeltaWindowSize(); + + /** + * Sets the Delta-Window-Size of this frame. + * The Delta-Window-Size must be positive. + */ + void setDeltaWindowSize(int deltaWindowSize); +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java index 03caefc4f6..3e08e7d89f 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java @@ -47,7 +47,7 @@ import org.junit.Test; public abstract class AbstractSocketSpdyEchoTest { private static final Random random = new Random(); - static final ChannelBuffer frames = ChannelBuffers.buffer(1160); + static final ChannelBuffer frames = ChannelBuffers.buffer(1176); static final int ignoredBytes = 20; private static ExecutorService executor; @@ -68,7 +68,7 @@ public abstract class AbstractSocketSpdyEchoTest { frames.writeInt(0); // SPDY Data Frame - frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeByte(0x01); frames.writeMedium(1024); for (int i = 0; i < 256; i ++) { @@ -81,7 +81,7 @@ public abstract class AbstractSocketSpdyEchoTest { frames.writeShort(1); frames.writeByte(0x03); frames.writeMedium(12); - frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeInt(random.nextInt() & 0x7FFFFFFF); frames.writeShort(0x8000); frames.writeShort(0); @@ -92,7 +92,7 @@ public abstract class AbstractSocketSpdyEchoTest { frames.writeShort(2); frames.writeByte(0x01); frames.writeMedium(8); - frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeInt(0); // SPDY RST_STREAM Frame @@ -100,7 +100,7 @@ public abstract class AbstractSocketSpdyEchoTest { frames.writeByte(2); frames.writeShort(3); frames.writeInt(8); - frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeInt(random.nextInt() | 0x01); // SPDY SETTINGS Frame @@ -133,7 +133,15 @@ public abstract class AbstractSocketSpdyEchoTest { frames.writeByte(2); frames.writeShort(8); frames.writeInt(4); - frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + + // SPDY WINDOW_UPDATE Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(9); + frames.writeInt(8); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); } @BeforeClass From 1a28d6119fb5d2e77caae8674540f62a6b9043e1 Mon Sep 17 00:00:00 2001 From: Jeff Pinner Date: Wed, 23 May 2012 08:54:11 -0700 Subject: [PATCH 3/3] SPDY: add SPDY/3 support --- .../codec/spdy/DefaultSpdyGoAwayFrame.java | 33 ++ .../codec/spdy/DefaultSpdyHeadersFrame.java | 12 + .../codec/spdy/DefaultSpdySynStreamFrame.java | 4 +- .../handler/codec/spdy/SpdyCodecUtil.java | 199 +++++++- .../handler/codec/spdy/SpdyFrameCodec.java | 17 +- .../handler/codec/spdy/SpdyFrameDecoder.java | 144 ++++-- .../handler/codec/spdy/SpdyFrameEncoder.java | 147 ++++-- .../handler/codec/spdy/SpdyGoAwayFrame.java | 10 + .../codec/spdy/SpdyHeaderBlockCompressor.java | 7 +- .../spdy/SpdyHeaderBlockDecompressor.java | 4 +- .../spdy/SpdyHeaderBlockJZlibCompressor.java | 13 +- .../spdy/SpdyHeaderBlockZlibCompressor.java | 12 +- .../spdy/SpdyHeaderBlockZlibDecompressor.java | 15 +- .../netty/handler/codec/spdy/SpdyHeaders.java | 213 ++++++-- .../handler/codec/spdy/SpdyHeadersFrame.java | 11 + .../handler/codec/spdy/SpdyHttpCodec.java | 7 +- .../handler/codec/spdy/SpdyHttpDecoder.java | 98 ++-- .../handler/codec/spdy/SpdyHttpEncoder.java | 83 ++- .../handler/codec/spdy/SpdyHttpHeaders.java | 25 + .../netty/handler/codec/spdy/SpdySession.java | 155 +++++- .../codec/spdy/SpdySessionHandler.java | 481 +++++++++++++++--- .../handler/codec/spdy/SpdySessionStatus.java | 113 ++++ .../handler/codec/spdy/SpdySettingsFrame.java | 17 +- .../handler/codec/spdy/SpdyStreamStatus.java | 32 ++ .../codec/spdy/SpdySynStreamFrame.java | 2 +- .../codec/spdy/SpdyWindowUpdateFrame.java | 2 +- .../spdy/AbstractSocketSpdyEchoTest.java | 91 +++- .../codec/spdy/SpdySessionHandlerTest.java | 10 +- 28 files changed, 1623 insertions(+), 334 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java index 1185e2f929..27d4a010ce 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java @@ -23,6 +23,7 @@ import io.netty.util.internal.StringUtil; public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { private int lastGoodStreamID; + private SpdySessionStatus status; /** * Creates a new instance. @@ -30,7 +31,28 @@ public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { * @param lastGoodStreamID the Last-good-stream-ID of this frame */ public DefaultSpdyGoAwayFrame(int lastGoodStreamID) { + this(lastGoodStreamID, 0); + } + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + * @param statusCode the Status code of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID, int statusCode) { + this(lastGoodStreamID, SpdySessionStatus.valueOf(statusCode)); + } + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + * @param status the status of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID, SpdySessionStatus status) { setLastGoodStreamID(lastGoodStreamID); + setStatus(status); } public int getLastGoodStreamID() { @@ -45,6 +67,14 @@ public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { this.lastGoodStreamID = lastGoodStreamID; } + public SpdySessionStatus getStatus() { + return status; + } + + public void setStatus(SpdySessionStatus status) { + this.status = status; + } + @Override public String toString() { StringBuilder buf = new StringBuilder(); @@ -52,6 +82,9 @@ public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { buf.append(StringUtil.NEWLINE); buf.append("--> Last-good-stream-ID = "); buf.append(lastGoodStreamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Status: "); + buf.append(status.toString()); return buf.toString(); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java index c779f9baf2..6ee8207402 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java @@ -24,6 +24,7 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyHeaderBlock implements SpdyHeadersFrame { private int streamID; + private boolean last; /** * Creates a new instance. @@ -47,10 +48,21 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyHeaderBlock this.streamID = streamID; } + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(streamID); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java index 548d8a7e72..cea7867b67 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java @@ -74,9 +74,9 @@ public class DefaultSpdySynStreamFrame extends DefaultSpdyHeaderBlock } public void setPriority(byte priority) { - if (priority < 0 || priority > 3) { + if (priority < 0 || priority > 7) { throw new IllegalArgumentException( - "Priortiy must be between 0 and 3 inclusive: " + priority); + "Priority must be between 0 and 7 inclusive: " + priority); } this.priority = priority; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java index 342fd36e51..7f323eed37 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -19,7 +19,8 @@ import io.netty.buffer.ChannelBuffer; final class SpdyCodecUtil { - static final int SPDY_VERSION = 2; + static final int SPDY_MIN_VERSION = 2; + static final int SPDY_MAX_VERSION = 3; static final int SPDY_HEADER_TYPE_OFFSET = 2; static final int SPDY_HEADER_FLAGS_OFFSET = 4; @@ -39,6 +40,7 @@ final class SpdyCodecUtil { static final int SPDY_GOAWAY_FRAME = 7; static final int SPDY_HEADERS_FRAME = 8; static final int SPDY_WINDOW_UPDATE_FRAME = 9; + static final int SPDY_CREDENTIAL_FRAME = 10; static final byte SPDY_FLAG_FIN = 0x01; static final byte SPDY_FLAG_UNIDIRECTIONAL = 0x02; @@ -52,7 +54,188 @@ final class SpdyCodecUtil { static final int SPDY_MAX_NV_LENGTH = 0xFFFF; // Length is a 16-bit field // Zlib Dictionary - private static final String SPDY_DICT_S = + static final byte[] SPDY_DICT = { + 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i + 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h + 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p + 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e + 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - - + 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - - + 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t - + 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e + 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c + 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - - + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e - + 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s + 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e - + 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w + 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o + 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r + 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n + 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e + 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - - + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t - + 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e + 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g + 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - - + 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - - + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t + 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - - + 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - - + 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - - + 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - - + 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t + 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i + 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f + 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i + 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h - + 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s + 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - - + 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e - + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - - + 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g + 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f - + 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i + 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e + 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t + 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e + 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - - + 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r + 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - - + 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a - + 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y + 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t + 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - - + 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - - + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - - + 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r + 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r + 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r - + 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e + 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e - + 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l + 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g - + 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a + 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s + 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t + 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y + 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a - + 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w + 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - - + 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d + 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t - + 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u + 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0 + 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - - + 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1 + 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r + 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b + 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s + 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i + 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e + 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e - + 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i + 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2 + 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5 + 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0 + 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3 + 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7 + 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0 + 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4 + 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1 + 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1 + 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4 + 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4 + 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N + 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o + 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e + 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a + 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 - + 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e + 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o + 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m + 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4 + 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0 + 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0 + 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d + 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N + 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d + 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e + 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r + 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o + 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t + 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e + 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 - + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e - + 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a + 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F + 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A + 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J + 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A + 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t - + 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v - + 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0 + 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n + 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W + 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u - + 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a + 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - - + 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k + 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t - + 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a + 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g + 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g + 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l + 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l + 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t + 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r + 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l + 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t + 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e + 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e + 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d + 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i + 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 - + 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - - + 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // - e n q - 0 - + }; + + private static final String SPDY2_DICT_S = "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + @@ -66,19 +249,19 @@ final class SpdyCodecUtil { "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + ".1statusversionurl "; - static final byte[] SPDY_DICT; + static final byte[] SPDY2_DICT; static { - byte[] SPDY_DICT_ = null; + byte[] SPDY2_DICT_ = null; try { - SPDY_DICT_ = SPDY_DICT_S.getBytes("US-ASCII"); + SPDY2_DICT_ = SPDY2_DICT_S.getBytes("US-ASCII"); // dictionary is null terminated - SPDY_DICT_[SPDY_DICT_.length - 1] = (byte) 0; + SPDY2_DICT_[SPDY2_DICT_.length - 1] = (byte) 0; } catch (Exception e) { - SPDY_DICT_ = new byte[1]; + SPDY2_DICT_ = new byte[1]; } - SPDY_DICT = SPDY_DICT_; + SPDY2_DICT = SPDY2_DICT_; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java index 35350bfafb..0c54e89e79 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java @@ -32,23 +32,24 @@ public class SpdyFrameCodec implements ChannelUpstreamHandler, private final SpdyFrameEncoder encoder; /** - * Creates a new instance with the default decoder and encoder options + * Creates a new instance with the specified {@code version} and + * the default decoder and encoder options * ({@code maxChunkSize (8192)}, {@code maxHeaderSize (16384)}, - * {@code compressionLevel (6)}, {@code windowBits (15)}, and - * {@code memLevel (8)}). + * {@code compressionLevel (6)}, {@code windowBits (15)}, + * and {@code memLevel (8)}). */ - public SpdyFrameCodec() { - this(8192, 16384, 6, 15, 8); + public SpdyFrameCodec(int version) { + this(version, 8192, 16384, 6, 15, 8); } /** * Creates a new instance with the specified decoder and encoder options. */ public SpdyFrameCodec( - int maxChunkSize, int maxHeaderSize, + int version, int maxChunkSize, int maxHeaderSize, int compressionLevel, int windowBits, int memLevel) { - decoder = new SpdyFrameDecoder(maxChunkSize, maxHeaderSize); - encoder = new SpdyFrameEncoder(compressionLevel, windowBits, memLevel); + decoder = new SpdyFrameDecoder(version, maxChunkSize, maxHeaderSize); + encoder = new SpdyFrameEncoder(version, compressionLevel, windowBits, memLevel); } public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java index 016aac0cc4..104e531862 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -30,11 +30,11 @@ import io.netty.handler.codec.frame.TooLongFrameException; */ public class SpdyFrameDecoder extends FrameDecoder { + private final int spdyVersion; private final int maxChunkSize; private final int maxHeaderSize; - private final SpdyHeaderBlockDecompressor headerBlockDecompressor = - SpdyHeaderBlockDecompressor.newInstance(); + private final SpdyHeaderBlockDecompressor headerBlockDecompressor; private State state; private SpdySettingsFrame spdySettingsFrame; @@ -64,18 +64,22 @@ public class SpdyFrameDecoder extends FrameDecoder { } /** - * Creates a new instance with the default {@code maxChunkSize (8192)} - * and {@code maxHeaderSize (16384)}. + * Creates a new instance with the specified {@code version} and the default + * {@code maxChunkSize (8192)} and {@code maxHeaderSize (16384)}. */ - public SpdyFrameDecoder() { - this(8192, 16384); + public SpdyFrameDecoder(int version) { + this(version, 8192, 16384); } /** * Creates a new instance with the specified parameters. */ - public SpdyFrameDecoder(int maxChunkSize, int maxHeaderSize) { + public SpdyFrameDecoder(int version, int maxChunkSize, int maxHeaderSize) { super(false); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } if (maxChunkSize <= 0) { throw new IllegalArgumentException( "maxChunkSize must be a positive integer: " + maxChunkSize); @@ -84,8 +88,10 @@ public class SpdyFrameDecoder extends FrameDecoder { throw new IllegalArgumentException( "maxHeaderSize must be a positive integer: " + maxHeaderSize); } + spdyVersion = version; this.maxChunkSize = maxChunkSize; this.maxHeaderSize = maxHeaderSize; + headerBlockDecompressor = SpdyHeaderBlockDecompressor.newInstance(version); state = State.READ_COMMON_HEADER; } @@ -109,7 +115,7 @@ public class SpdyFrameDecoder extends FrameDecoder { case READ_COMMON_HEADER: state = readCommonHeader(buffer); if (state == State.FRAME_ERROR) { - if (version != SPDY_VERSION) { + if (version != spdyVersion) { fireProtocolException(ctx, "Unsupported version: " + version); } else { fireInvalidControlFrameException(ctx); @@ -175,13 +181,21 @@ public class SpdyFrameDecoder extends FrameDecoder { int readableEntries = Math.min(buffer.readableBytes() >> 3, length >> 3); for (int i = 0; i < readableEntries; i ++) { - // Chromium Issue 79156 - // SPDY setting ids are not written in network byte order - // Read id assuming the architecture is little endian - int ID = buffer.readByte() & 0xFF | + int ID; + byte ID_flags; + if (version < 3) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Read id assuming the architecture is little endian + ID = buffer.readByte() & 0xFF | (buffer.readByte() & 0xFF) << 8 | (buffer.readByte() & 0xFF) << 16; - byte ID_flags = buffer.readByte(); + ID_flags = buffer.readByte(); + } else { + ID_flags = buffer.readByte(); + ID = getUnsignedMedium(buffer, buffer.readerIndex()); + buffer.skipBytes(3); + } int value = getSignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(4); @@ -327,7 +341,7 @@ public class SpdyFrameDecoder extends FrameDecoder { type = getUnsignedShort(buffer, typeOffset); // Check version first then validity - if (version != SPDY_VERSION || !isValidControlFrameHeader()) { + if (version != spdyVersion || !isValidControlFrameHeader()) { return State.FRAME_ERROR; } @@ -364,6 +378,7 @@ public class SpdyFrameDecoder extends FrameDecoder { private Object readControlFrame(ChannelBuffer buffer) { int streamID; + int statusCode; switch (type) { case SPDY_RST_STREAM_FRAME: if (buffer.readableBytes() < 8) { @@ -371,7 +386,7 @@ public class SpdyFrameDecoder extends FrameDecoder { } streamID = getUnsignedInt(buffer, buffer.readerIndex()); - int statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); + statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); buffer.skipBytes(8); return new DefaultSpdyRstStreamFrame(streamID, statusCode); @@ -387,14 +402,22 @@ public class SpdyFrameDecoder extends FrameDecoder { return new DefaultSpdyPingFrame(ID); case SPDY_GOAWAY_FRAME: - if (buffer.readableBytes() < 4) { + int minLength = version < 3 ? 4 : 8; + if (buffer.readableBytes() < minLength) { return null; } int lastGoodStreamID = getUnsignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(4); - return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + if (version < 3) { + return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + } + + statusCode = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + + return new DefaultSpdyGoAwayFrame(lastGoodStreamID, statusCode); case SPDY_WINDOW_UPDATE_FRAME: if (buffer.readableBytes() < 8) { @@ -413,22 +436,27 @@ public class SpdyFrameDecoder extends FrameDecoder { } private SpdyHeaderBlock readHeaderBlockFrame(ChannelBuffer buffer) { + int minLength; int streamID; switch (type) { case SPDY_SYN_STREAM_FRAME: - if (buffer.readableBytes() < 12) { + minLength = version < 3 ? 12 : 10; + if (buffer.readableBytes() < minLength) { return null; } int offset = buffer.readerIndex(); streamID = getUnsignedInt(buffer, offset); int associatedToStreamID = getUnsignedInt(buffer, offset + 4); - byte priority = (byte) (buffer.getByte(offset + 8) >> 6 & 0x03); + byte priority = (byte) (buffer.getByte(offset + 8) >> 5 & 0x07); + if (version < 3) { + priority >>= 1; + } buffer.skipBytes(10); length -= 10; // SPDY/2 requires 16-bits of padding for empty header blocks - if (length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { buffer.skipBytes(2); length = 0; } @@ -441,16 +469,23 @@ public class SpdyFrameDecoder extends FrameDecoder { return spdySynStreamFrame; case SPDY_SYN_REPLY_FRAME: - if (buffer.readableBytes() < 8) { + minLength = version < 3 ? 8 : 4; + if (buffer.readableBytes() < minLength) { return null; } streamID = getUnsignedInt(buffer, buffer.readerIndex()); - buffer.skipBytes(6); - length -= 6; + buffer.skipBytes(4); + length -= 4; + + // SPDY/2 has 16-bits of unused space + if (version < 3) { + buffer.skipBytes(2); + length -= 2; + } // SPDY/2 requires 16-bits of padding for empty header blocks - if (length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { buffer.skipBytes(2); length = 0; } @@ -461,9 +496,12 @@ public class SpdyFrameDecoder extends FrameDecoder { return spdySynReplyFrame; case SPDY_HEADERS_FRAME: - // Protocol allows length 4 frame when there are no name/value pairs - int minLength = length == 4 ? 4 : 8; - if (buffer.readableBytes() < minLength) { + if (buffer.readableBytes() < 4) { + return null; + } + + // SPDY/2 allows length 4 frame when there are no name/value pairs + if (version < 3 && length > 4 && buffer.readableBytes() < 8) { return null; } @@ -471,16 +509,22 @@ public class SpdyFrameDecoder extends FrameDecoder { buffer.skipBytes(4); length -= 4; - // SPDY/2 requires 16-bits of padding for empty header blocks - if (length == 4 && buffer.getShort(buffer.readerIndex() + 2) == 0) { - buffer.skipBytes(4); - length = 0; - } else if (length != 0) { + // SPDY/2 has 16-bits of unused space + if (version < 3 && length != 0) { buffer.skipBytes(2); length -= 2; } - return new DefaultSpdyHeadersFrame(streamID); + // SPDY/2 requires 16-bits of padding for empty header blocks + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamID); + spdyHeadersFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + + return spdyHeadersFrame; default: throw new Error("Shouldn't reach here."); @@ -497,7 +541,11 @@ public class SpdyFrameDecoder extends FrameDecoder { } private int readLengthField() { - return decompressed.readUnsignedShort(); + if (version < 3) { + return decompressed.readUnsignedShort(); + } else { + return decompressed.readInt(); + } } private void decodeHeaderBlock(ChannelBuffer buffer) throws Exception { @@ -519,7 +567,7 @@ public class SpdyFrameDecoder extends FrameDecoder { return; } - int lengthFieldSize = 2; // SPDY/2 uses 16-bit length fields + int lengthFieldSize = version < 3 ? 2 : 4; if (numHeaders == -1) { // Read number of Name/Value pairs @@ -527,6 +575,10 @@ public class SpdyFrameDecoder extends FrameDecoder { return; } numHeaders = readLengthField(); + if (numHeaders < 0) { + spdyHeaderBlock.setInvalid(); + return; + } } while (numHeaders > 0) { @@ -542,7 +594,7 @@ public class SpdyFrameDecoder extends FrameDecoder { int nameLength = readLengthField(); // Recipients of a zero-length name must issue a stream error - if (nameLength == 0) { + if (nameLength <= 0) { spdyHeaderBlock.setInvalid(); return; } @@ -577,7 +629,7 @@ public class SpdyFrameDecoder extends FrameDecoder { int valueLength = readLengthField(); // Recipients of illegal value fields must issue a stream error - if (valueLength == 0) { + if (valueLength <= 0) { spdyHeaderBlock.setInvalid(); return; } @@ -630,10 +682,10 @@ public class SpdyFrameDecoder extends FrameDecoder { private boolean isValidControlFrameHeader() { switch (type) { case SPDY_SYN_STREAM_FRAME: - return length >= 12; + return version < 3 ? length >= 12 : length >= 10; case SPDY_SYN_REPLY_FRAME: - return length >= 8; + return version < 3 ? length >= 8 : length >= 4; case SPDY_RST_STREAM_FRAME: return flags == 0 && length == 8; @@ -648,14 +700,19 @@ public class SpdyFrameDecoder extends FrameDecoder { return length == 4; case SPDY_GOAWAY_FRAME: - return length == 4; + return version < 3 ? length == 4 : length == 8; case SPDY_HEADERS_FRAME: - return length == 4 || length >= 8; + if (version < 3) { + return length == 4 || length >= 8; + } else { + return length >= 4; + } case SPDY_WINDOW_UPDATE_FRAME: return length == 8; + case SPDY_CREDENTIAL_FRAME: default: return true; } @@ -674,6 +731,7 @@ public class SpdyFrameDecoder extends FrameDecoder { return true; case SPDY_NOOP_FRAME: + case SPDY_CREDENTIAL_FRAME: default: return false; } @@ -717,6 +775,10 @@ public class SpdyFrameDecoder extends FrameDecoder { case SPDY_WINDOW_UPDATE_FRAME: message = "Received invalid WINDOW_UPDATE control frame"; break; + + case SPDY_CREDENTIAL_FRAME: + message = "Received invalid CREDENTIAL control frame"; + break; } fireProtocolException(ctx, message); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java index 309c3551e3..88dfa4a70d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -33,23 +33,31 @@ import io.netty.handler.codec.oneone.OneToOneEncoder; */ public class SpdyFrameEncoder extends OneToOneEncoder { + private final int version; private volatile boolean finished; private final SpdyHeaderBlockCompressor headerBlockCompressor; /** - * Creates a new instance with the default {@code compressionLevel (6)}, - * {@code windowBits (15)}, and {@code memLevel (8)}. + * Creates a new instance with the specified {@code version} and the + * default {@code compressionLevel (6)}, {@code windowBits (15)}, + * and {@code memLevel (8)}. */ - public SpdyFrameEncoder() { - this(6, 15, 8); + public SpdyFrameEncoder(int version) { + this(version, 6, 15, 8); } /** * Creates a new instance with the specified parameters. */ - public SpdyFrameEncoder(int compressionLevel, int windowBits, int memLevel) { + public SpdyFrameEncoder(int version, int compressionLevel, int windowBits, int memLevel) { super(); - headerBlockCompressor = SpdyHeaderBlockCompressor.newInstance(compressionLevel, windowBits, memLevel); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unknown version: " + version); + } + this.version = version; + headerBlockCompressor = SpdyHeaderBlockCompressor.newInstance( + version, compressionLevel, windowBits, memLevel); } @Override @@ -93,23 +101,37 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; ChannelBuffer data = compressHeaderBlock( - encodeHeaderBlock(spdySynStreamFrame)); + encodeHeaderBlock(version, spdySynStreamFrame)); byte flags = spdySynStreamFrame.isLast() ? SPDY_FLAG_FIN : 0; if (spdySynStreamFrame.isUnidirectional()) { flags |= SPDY_FLAG_UNIDIRECTIONAL; } int headerBlockLength = data.readableBytes(); - int length = headerBlockLength == 0 ? 12 : 10 + headerBlockLength; + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 12 : 10 + headerBlockLength; + } else { + length = 10 + headerBlockLength; + } ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_SYN_STREAM_FRAME); frame.writeByte(flags); frame.writeMedium(length); frame.writeInt(spdySynStreamFrame.getStreamID()); frame.writeInt(spdySynStreamFrame.getAssociatedToStreamID()); - frame.writeShort((spdySynStreamFrame.getPriority() & 0xFF) << 14); - if (data.readableBytes() == 0) { + if (version < 3) { + // Restrict priorities for SPDY/2 to between 0 and 3 + byte priority = spdySynStreamFrame.getPriority(); + if (priority > 3) { + priority = 3; + } + frame.writeShort((priority & 0xFF) << 14); + } else { + frame.writeShort((spdySynStreamFrame.getPriority() & 0xFF) << 13); + } + if (version < 3 && data.readableBytes() == 0) { frame.writeShort(0); } return ChannelBuffers.wrappedBuffer(frame, data); @@ -118,21 +140,28 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; ChannelBuffer data = compressHeaderBlock( - encodeHeaderBlock(spdySynReplyFrame)); + encodeHeaderBlock(version, spdySynReplyFrame)); byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; int headerBlockLength = data.readableBytes(); - int length = headerBlockLength == 0 ? 8 : 6 + headerBlockLength; + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 8 : 6 + headerBlockLength; + } else { + length = 4 + headerBlockLength; + } ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_SYN_REPLY_FRAME); frame.writeByte(flags); frame.writeMedium(length); frame.writeInt(spdySynReplyFrame.getStreamID()); - if (data.readableBytes() == 0) { - frame.writeInt(0); - } else { - frame.writeShort(0); + if (version < 3) { + if (data.readableBytes() == 0) { + frame.writeInt(0); + } else { + frame.writeShort(0); + } } return ChannelBuffers.wrappedBuffer(frame, data); @@ -141,7 +170,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 8); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_RST_STREAM_FRAME); frame.writeInt(8); frame.writeInt(spdyRstStreamFrame.getStreamID()); @@ -158,7 +187,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { int length = 4 + numEntries * 8; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_SETTINGS_FRAME); frame.writeByte(flags); frame.writeMedium(length); @@ -172,13 +201,18 @@ public class SpdyFrameEncoder extends OneToOneEncoder { if (spdySettingsFrame.isPersisted(id)) { ID_flags |= SPDY_SETTINGS_PERSISTED; } - // Chromium Issue 79156 - // SPDY setting ids are not written in network byte order - // Write id assuming the architecture is little endian - frame.writeByte(id >> 0 & 0xFF); - frame.writeByte(id >> 8 & 0xFF); - frame.writeByte(id >> 16 & 0xFF); - frame.writeByte(ID_flags); + if (version < 3) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Write id assuming the architecture is little endian + frame.writeByte(id >> 0 & 0xFF); + frame.writeByte(id >> 8 & 0xFF); + frame.writeByte(id >> 16 & 0xFF); + frame.writeByte(ID_flags); + } else { + frame.writeByte(ID_flags); + frame.writeMedium(id); + } frame.writeInt(spdySettingsFrame.getValue(id)); } return frame; @@ -187,7 +221,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_NOOP_FRAME); frame.writeInt(0); return frame; @@ -197,7 +231,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_PING_FRAME); frame.writeInt(4); frame.writeInt(spdyPingFrame.getID()); @@ -206,28 +240,39 @@ public class SpdyFrameEncoder extends OneToOneEncoder { } else if (msg instanceof SpdyGoAwayFrame) { SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + int length = version < 3 ? 4 : 8; ChannelBuffer frame = ChannelBuffers.buffer( - ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); - frame.writeShort(SPDY_VERSION | 0x8000); + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_GOAWAY_FRAME); - frame.writeInt(4); + frame.writeInt(length); frame.writeInt(spdyGoAwayFrame.getLastGoodStreamID()); + if (version >= 3) { + frame.writeInt(spdyGoAwayFrame.getStatus().getCode()); + } return frame; } else if (msg instanceof SpdyHeadersFrame) { SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; ChannelBuffer data = compressHeaderBlock( - encodeHeaderBlock(spdyHeadersFrame)); + encodeHeaderBlock(version, spdyHeadersFrame)); + byte flags = spdyHeadersFrame.isLast() ? SPDY_FLAG_FIN : 0; int headerBlockLength = data.readableBytes(); - int length = headerBlockLength == 0 ? 4 : 6 + headerBlockLength; + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 4 : 6 + headerBlockLength; + } else { + length = 4 + headerBlockLength; + } ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_HEADERS_FRAME); - frame.writeInt(length); + frame.writeByte(flags); + frame.writeMedium(length); frame.writeInt(spdyHeadersFrame.getStreamID()); - if (data.readableBytes() != 0) { + if (version < 3 && data.readableBytes() != 0) { frame.writeShort(0); } return ChannelBuffers.wrappedBuffer(frame, data); @@ -237,7 +282,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; ChannelBuffer frame = ChannelBuffers.buffer( ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 8); - frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(version | 0x8000); frame.writeShort(SPDY_WINDOW_UPDATE_FRAME); frame.writeInt(8); frame.writeInt(spdyWindowUpdateFrame.getStreamID()); @@ -249,7 +294,23 @@ public class SpdyFrameEncoder extends OneToOneEncoder { return msg; } - private static ChannelBuffer encodeHeaderBlock(SpdyHeaderBlock headerFrame) + private static void writeLengthField(int version, ChannelBuffer buffer, int length) { + if (version < 3) { + buffer.writeShort(length); + } else { + buffer.writeInt(length); + } + } + + private static void setLengthField(int version, ChannelBuffer buffer, int writerIndex, int length) { + if (version < 3) { + buffer.setShort(writerIndex, length); + } else { + buffer.setInt(writerIndex, length); + } + } + + private static ChannelBuffer encodeHeaderBlock(int version, SpdyHeaderBlock headerFrame) throws Exception { Set names = headerFrame.getHeaderNames(); int numHeaders = names.size(); @@ -262,14 +323,14 @@ public class SpdyFrameEncoder extends OneToOneEncoder { } ChannelBuffer headerBlock = ChannelBuffers.dynamicBuffer( ByteOrder.BIG_ENDIAN, 256); - headerBlock.writeShort(numHeaders); + writeLengthField(version, headerBlock, numHeaders); for (String name: names) { byte[] nameBytes = name.getBytes("UTF-8"); - headerBlock.writeShort(nameBytes.length); + writeLengthField(version, headerBlock, nameBytes.length); headerBlock.writeBytes(nameBytes); int savedIndex = headerBlock.writerIndex(); int valueLength = 0; - headerBlock.writeShort(valueLength); + writeLengthField(version, headerBlock, valueLength); for (String value: headerFrame.getHeaders(name)) { byte[] valueBytes = value.getBytes("UTF-8"); headerBlock.writeBytes(valueBytes); @@ -281,7 +342,7 @@ public class SpdyFrameEncoder extends OneToOneEncoder { throw new IllegalArgumentException( "header exceeds allowable length: " + name); } - headerBlock.setShort(savedIndex, valueLength); + setLengthField(version, headerBlock, savedIndex, valueLength); headerBlock.writerIndex(headerBlock.writerIndex() - 1); } return headerBlock; diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java index 890e464406..6abb5080a8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java @@ -30,4 +30,14 @@ public interface SpdyGoAwayFrame { * cannot be negative. */ void setLastGoodStreamID(int lastGoodStreamID); + + /** + * Returns the status of this frame. + */ + SpdySessionStatus getStatus(); + + /** + * Sets the status of this frame. + */ + void setStatus(SpdySessionStatus status); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java index af51caf6b1..57f8060304 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java @@ -21,13 +21,14 @@ import io.netty.util.internal.DetectionUtil; abstract class SpdyHeaderBlockCompressor { static SpdyHeaderBlockCompressor newInstance( - int compressionLevel, int windowBits, int memLevel) { + int version, int compressionLevel, int windowBits, int memLevel) { if (DetectionUtil.javaVersion() >= 7) { - return new SpdyHeaderBlockZlibCompressor(compressionLevel); + return new SpdyHeaderBlockZlibCompressor( + version, compressionLevel); } else { return new SpdyHeaderBlockJZlibCompressor( - compressionLevel, windowBits, memLevel); + version, compressionLevel, windowBits, memLevel); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java index 23b9f4a04b..b861249d83 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java @@ -19,8 +19,8 @@ import io.netty.buffer.ChannelBuffer; abstract class SpdyHeaderBlockDecompressor { - static SpdyHeaderBlockDecompressor newInstance() { - return new SpdyHeaderBlockZlibDecompressor(); + static SpdyHeaderBlockDecompressor newInstance(int version) { + return new SpdyHeaderBlockZlibDecompressor(version); } abstract void setInput(ChannelBuffer compressed); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java index dc6e7db1ff..b7236fab0a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java @@ -26,7 +26,12 @@ class SpdyHeaderBlockJZlibCompressor extends SpdyHeaderBlockCompressor { private final ZStream z = new ZStream(); - public SpdyHeaderBlockJZlibCompressor(int compressionLevel, int windowBits, int memLevel) { + public SpdyHeaderBlockJZlibCompressor( + int version, int compressionLevel, int windowBits, int memLevel) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } if (compressionLevel < 0 || compressionLevel > 9) { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); @@ -46,7 +51,11 @@ class SpdyHeaderBlockJZlibCompressor extends SpdyHeaderBlockCompressor { throw new CompressionException( "failed to initialize an SPDY header block deflater: " + resultCode); } else { - resultCode = z.deflateSetDictionary(SPDY_DICT, SPDY_DICT.length); + if (version < 3) { + resultCode = z.deflateSetDictionary(SPDY2_DICT, SPDY2_DICT.length); + } else { + resultCode = z.deflateSetDictionary(SPDY_DICT, SPDY_DICT.length); + } if (resultCode != JZlib.Z_OK) { throw new CompressionException( "failed to set the SPDY dictionary: " + resultCode); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java index d00434b120..dad8ee647f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java @@ -26,13 +26,21 @@ class SpdyHeaderBlockZlibCompressor extends SpdyHeaderBlockCompressor { private final byte[] out = new byte[8192]; private final Deflater compressor; - public SpdyHeaderBlockZlibCompressor(int compressionLevel) { + public SpdyHeaderBlockZlibCompressor(int version, int compressionLevel) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } if (compressionLevel < 0 || compressionLevel > 9) { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); } compressor = new Deflater(compressionLevel); - compressor.setDictionary(SPDY_DICT); + if (version < 3) { + compressor.setDictionary(SPDY2_DICT); + } else { + compressor.setDictionary(SPDY_DICT); + } } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java index 87f5ba09e9..1fa8e7b038 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java @@ -24,9 +24,18 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; class SpdyHeaderBlockZlibDecompressor extends SpdyHeaderBlockDecompressor { + private final int version; private final byte[] out = new byte[8192]; private final Inflater decompressor = new Inflater(); + public SpdyHeaderBlockZlibDecompressor(int version) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + this.version = version; + } + @Override public void setInput(ChannelBuffer compressed) { byte[] in = new byte[compressed.readableBytes()]; @@ -39,7 +48,11 @@ class SpdyHeaderBlockZlibDecompressor extends SpdyHeaderBlockDecompressor { try { int numBytes = decompressor.inflate(out); if (numBytes == 0 && decompressor.needsDictionary()) { - decompressor.setDictionary(SPDY_DICT); + if (version < 3) { + decompressor.setDictionary(SPDY2_DICT); + } else { + decompressor.setDictionary(SPDY_DICT); + } numBytes = decompressor.inflate(out); } decompressed.writeBytes(out, 0, numBytes); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java index 6236931c0c..b5001688a7 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java @@ -27,7 +27,7 @@ import io.netty.handler.codec.http.HttpVersion; /** * Provides the constants for the standard SPDY HTTP header names and commonly - * used utility methods that access an {@link SpdyHeaderBlock}. + * used utility methods that access a {@link SpdyHeaderBlock}. * @apiviz.stereotype static */ public class SpdyHeaders { @@ -37,6 +37,41 @@ public class SpdyHeaders { * @apiviz.stereotype static */ public static final class HttpNames { + /** + * {@code ":host"} + */ + public static final String HOST = ":host"; + /** + * {@code ":method"} + */ + public static final String METHOD = ":method"; + /** + * {@code ":path"} + */ + public static final String PATH = ":path"; + /** + * {@code ":scheme"} + */ + public static final String SCHEME = ":scheme"; + /** + * {@code ":status"} + */ + public static final String STATUS = ":status"; + /** + * {@code ":version"} + */ + public static final String VERSION = ":version"; + + private HttpNames() { + super(); + } + } + + /** + * SPDY/2 HTTP header names + * @apiviz.stereotype static + */ + public static final class Spdy2HttpNames { /** * {@code "method"} */ @@ -58,7 +93,7 @@ public class SpdyHeaders { */ public static final String VERSION = "version"; - private HttpNames() { + private Spdy2HttpNames() { super(); } } @@ -115,64 +150,118 @@ public class SpdyHeaders { } /** - * Removes the {@code "method"} header. + * Removes the SPDY host header. */ - public static void removeMethod(SpdyHeaderBlock block) { - block.removeHeader(HttpNames.METHOD); + public static void removeHost(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.HOST); } /** - * Returns the {@link HttpMethod} represented by the {@code "method"} header. + * Returns the SPDY host header. */ - public static HttpMethod getMethod(SpdyHeaderBlock block) { + public static String getHost(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.HOST); + } + + /** + * Set the SPDY host header. + */ + public static void setHost(SpdyHeaderBlock block, String host) { + block.setHeader(HttpNames.HOST, host); + } + + /** + * Removes the HTTP method header. + */ + public static void removeMethod(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.METHOD); + } else { + block.removeHeader(HttpNames.METHOD); + } + } + + /** + * Returns the {@link HttpMethod} represented by the HTTP method header. + */ + public static HttpMethod getMethod(int spdyVersion, SpdyHeaderBlock block) { try { - return HttpMethod.valueOf(block.getHeader(HttpNames.METHOD)); + if (spdyVersion < 3) { + return HttpMethod.valueOf(block.getHeader(Spdy2HttpNames.METHOD)); + } else { + return HttpMethod.valueOf(block.getHeader(HttpNames.METHOD)); + } } catch (Exception e) { return null; } } /** - * Sets the {@code "method"} header. + * Sets the HTTP method header. */ - public static void setMethod(SpdyHeaderBlock block, HttpMethod method) { - block.setHeader(HttpNames.METHOD, method.getName()); + public static void setMethod(int spdyVersion, SpdyHeaderBlock block, HttpMethod method) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.METHOD, method.getName()); + } else { + block.setHeader(HttpNames.METHOD, method.getName()); + } } /** - * Removes the {@code "scheme"} header. + * Removes the URL scheme header. */ - public static void removeScheme(SpdyHeaderBlock block) { - block.removeHeader(HttpNames.SCHEME); + public static void removeScheme(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 2) { + block.removeHeader(Spdy2HttpNames.SCHEME); + } else { + block.removeHeader(HttpNames.SCHEME); + } } /** - * Returns the value of the {@code "scheme"} header. + * Returns the value of the URL scheme header. */ - public static String getScheme(SpdyHeaderBlock block) { - return block.getHeader(HttpNames.SCHEME); + public static String getScheme(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + return block.getHeader(Spdy2HttpNames.SCHEME); + } else { + return block.getHeader(HttpNames.SCHEME); + } } /** - * Sets the {@code "scheme"} header. + * Sets the URL scheme header. */ - public static void setScheme(SpdyHeaderBlock block, String value) { - block.setHeader(HttpNames.SCHEME, value); + public static void setScheme(int spdyVersion, SpdyHeaderBlock block, String scheme) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.SCHEME, scheme); + } else { + block.setHeader(HttpNames.SCHEME, scheme); + } } /** - * Removes the {@code "status"} header. + * Removes the HTTP response status header. */ - public static void removeStatus(SpdyHeaderBlock block) { - block.removeHeader(HttpNames.STATUS); + public static void removeStatus(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.STATUS); + } else { + block.removeHeader(HttpNames.STATUS); + } } /** - * Returns the {@link HttpResponseStatus} represented by the {@code "status"} header. + * Returns the {@link HttpResponseStatus} represented by the HTTP response status header. */ - public static HttpResponseStatus getStatus(SpdyHeaderBlock block) { + public static HttpResponseStatus getStatus(int spdyVersion, SpdyHeaderBlock block) { try { - String status = block.getHeader(HttpNames.STATUS); + String status; + if (spdyVersion < 3) { + status = block.getHeader(Spdy2HttpNames.STATUS); + } else { + status = block.getHeader(HttpNames.STATUS); + } int space = status.indexOf(' '); if (space == -1) { return HttpResponseStatus.valueOf(Integer.parseInt(status)); @@ -180,7 +269,7 @@ public class SpdyHeaders { int code = Integer.parseInt(status.substring(0, space)); String reasonPhrase = status.substring(space + 1); HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(code); - if (responseStatus.getReasonPhrase().equals(responseStatus)) { + if (responseStatus.getReasonPhrase().equals(reasonPhrase)) { return responseStatus; } else { return new HttpResponseStatus(code, reasonPhrase); @@ -192,56 +281,84 @@ public class SpdyHeaders { } /** - * Sets the {@code "status"} header. + * Sets the HTTP response status header. */ - public static void setStatus(SpdyHeaderBlock block, HttpResponseStatus status) { - block.setHeader(HttpNames.STATUS, status.toString()); + public static void setStatus(int spdyVersion, SpdyHeaderBlock block, HttpResponseStatus status) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.STATUS, status.toString()); + } else { + block.setHeader(HttpNames.STATUS, status.toString()); + } } /** - * Removes the {@code "url"} header. + * Removes the URL path header. */ - public static void removeUrl(SpdyHeaderBlock block) { - block.removeHeader(HttpNames.URL); + public static void removeUrl(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.URL); + } else { + block.removeHeader(HttpNames.PATH); + } } /** - * Returns the value of the {@code "url"} header. + * Returns the value of the URL path header. */ - public static String getUrl(SpdyHeaderBlock block) { - return block.getHeader(HttpNames.URL); + public static String getUrl(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + return block.getHeader(Spdy2HttpNames.URL); + } else { + return block.getHeader(HttpNames.PATH); + } } /** - * Sets the {@code "url"} header. + * Sets the URL path header. */ - public static void setUrl(SpdyHeaderBlock block, String value) { - block.setHeader(HttpNames.URL, value); + public static void setUrl(int spdyVersion, SpdyHeaderBlock block, String path) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.URL, path); + } else { + block.setHeader(HttpNames.PATH, path); + } } /** - * Removes the {@code "version"} header. + * Removes the HTTP version header. */ - public static void removeVersion(SpdyHeaderBlock block) { - block.removeHeader(HttpNames.VERSION); + public static void removeVersion(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.VERSION); + } else { + block.removeHeader(HttpNames.VERSION); + } } /** - * Returns the {@link HttpVersion} represented by the {@code "version"} header. + * Returns the {@link HttpVersion} represented by the HTTP version header. */ - public static HttpVersion getVersion(SpdyHeaderBlock block) { + public static HttpVersion getVersion(int spdyVersion, SpdyHeaderBlock block) { try { - return HttpVersion.valueOf(block.getHeader(HttpNames.VERSION)); + if (spdyVersion < 3) { + return HttpVersion.valueOf(block.getHeader(Spdy2HttpNames.VERSION)); + } else { + return HttpVersion.valueOf(block.getHeader(HttpNames.VERSION)); + } } catch (Exception e) { return null; } } /** - * Sets the {@code "version"} header. + * Sets the HTTP version header. */ - public static void setVersion(SpdyHeaderBlock block, HttpVersion version) { - block.setHeader(HttpNames.VERSION, version.getText()); + public static void setVersion(int spdyVersion, SpdyHeaderBlock block, HttpVersion httpVersion) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.VERSION, httpVersion.getText()); + } else { + block.setHeader(HttpNames.VERSION, httpVersion.getText()); + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java index 4d1dac9d11..f136f378d7 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java @@ -29,4 +29,15 @@ public interface SpdyHeadersFrame extends SpdyHeaderBlock { * Sets the Stream-ID of this frame. The Stream-ID must be positive. */ void setStreamID(int streamID); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java index 1e56429002..536b881a27 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java @@ -28,13 +28,14 @@ import io.netty.channel.ChannelUpstreamHandler; public class SpdyHttpCodec implements ChannelUpstreamHandler, ChannelDownstreamHandler { private final SpdyHttpDecoder decoder; - private final SpdyHttpEncoder encoder = new SpdyHttpEncoder(); + private final SpdyHttpEncoder encoder; /** * Creates a new instance with the specified decoder options. */ - public SpdyHttpCodec(int maxContentLength) { - decoder = new SpdyHttpDecoder(maxContentLength); + public SpdyHttpCodec(int version, int maxContentLength) { + decoder = new SpdyHttpDecoder(version, maxContentLength); + encoder = new SpdyHttpEncoder(version); } public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java index 0a6245bc9a..af13da2161 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,22 +44,29 @@ import io.netty.handler.codec.oneone.OneToOneDecoder; */ public class SpdyHttpDecoder extends OneToOneDecoder { + private final int spdyVersion; private final int maxContentLength; private final Map messageMap = new HashMap(); /** * Creates a new instance. * + * @param version the protocol version * @param maxContentLength the maximum length of the message content. * If the length of the message content exceeds this value, * a {@link TooLongFrameException} will be raised. */ - public SpdyHttpDecoder(int maxContentLength) { + public SpdyHttpDecoder(int version, int maxContentLength) { super(); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } if (maxContentLength <= 0) { throw new IllegalArgumentException( "maxContentLength must be a positive integer: " + maxContentLength); } + spdyVersion = version; this.maxContentLength = maxContentLength; } @@ -72,7 +81,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { int streamID = spdySynStreamFrame.getStreamID(); if (SpdyCodecUtil.isServerID(streamID)) { - // SYN_STREAM frames inititated by the server are pushed resources + // SYN_STREAM frames initiated by the server are pushed resources int associatedToStreamID = spdySynStreamFrame.getAssociatedToStreamID(); // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0 @@ -83,7 +92,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame); } - String URL = SpdyHeaders.getUrl(spdySynStreamFrame); + String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame); // If a client receives a SYN_STREAM without a 'url' header // it must reply with a RST_STREAM with error code PROTOCOL_ERROR @@ -94,7 +103,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { } try { - HttpResponse httpResponse = createHttpResponse(spdySynStreamFrame); + HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame); // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers SpdyHttpHeaders.setStreamID(httpResponse, streamID); @@ -118,7 +127,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { } else { // SYN_STREAM frames initiated by the client are HTTP requests try { - HttpRequest httpRequest = createHttpRequest(spdySynStreamFrame); + HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame); // Set the Stream-ID as a header SpdyHttpHeaders.setStreamID(httpRequest, streamID); @@ -130,13 +139,13 @@ public class SpdyHttpDecoder extends OneToOneDecoder { messageMap.put(new Integer(streamID), httpRequest); } } catch (Exception e) { - // If a client sends a SYN_STREAM without method, url, and version headers - // the server must reply with a HTTP 400 BAD REQUEST reply + // If a client sends a SYN_STREAM without all of the method, url (host and path), + // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); spdySynReplyFrame.setLast(true); - SpdyHeaders.setStatus(spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST); - SpdyHeaders.setVersion(spdySynReplyFrame, HttpVersion.HTTP_1_0); + SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST); + SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0); Channels.write(ctx, Channels.future(channel), spdySynReplyFrame); } } @@ -147,7 +156,7 @@ public class SpdyHttpDecoder extends OneToOneDecoder { int streamID = spdySynReplyFrame.getStreamID(); try { - HttpResponse httpResponse = createHttpResponse(spdySynReplyFrame); + HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame); // Set the Stream-ID as a header SpdyHttpHeaders.setStreamID(httpResponse, streamID); @@ -215,67 +224,72 @@ public class SpdyHttpDecoder extends OneToOneDecoder { messageMap.remove(streamID); return httpMessage; } + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + Integer streamID = new Integer(spdyRstStreamFrame.getStreamID()); + messageMap.remove(streamID); } return null; } - private static HttpRequest createHttpRequest(SpdyHeaderBlock requestFrame) + private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeaderBlock requestFrame) throws Exception { // Create the first line of the request from the name/value pairs - HttpMethod method = SpdyHeaders.getMethod(requestFrame); - String url = SpdyHeaders.getUrl(requestFrame); - HttpVersion version = SpdyHeaders.getVersion(requestFrame); - SpdyHeaders.removeMethod(requestFrame); - SpdyHeaders.removeUrl(requestFrame); - SpdyHeaders.removeVersion(requestFrame); + HttpMethod method = SpdyHeaders.getMethod(spdyVersion, requestFrame); + String url = SpdyHeaders.getUrl(spdyVersion, requestFrame); + HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame); + SpdyHeaders.removeMethod(spdyVersion, requestFrame); + SpdyHeaders.removeUrl(spdyVersion, requestFrame); + SpdyHeaders.removeVersion(spdyVersion, requestFrame); - HttpRequest httpRequest = new DefaultHttpRequest(version, method, url); - for (Map.Entry e: requestFrame.getHeaders()) { - httpRequest.addHeader(e.getKey(), e.getValue()); + HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url); + + // Remove the scheme header + SpdyHeaders.removeScheme(spdyVersion, requestFrame); + + if (spdyVersion >= 3) { + // Replace the SPDY host header with the HTTP host header + String host = SpdyHeaders.getHost(requestFrame); + SpdyHeaders.removeHost(requestFrame); + HttpHeaders.setHost(httpRequest, host); } - // Chunked encoding is no longer valid - List encodings = httpRequest.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING); - encodings.remove(HttpHeaders.Values.CHUNKED); - if (encodings.isEmpty()) { - httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); - } else { - httpRequest.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, encodings); + for (Map.Entry e: requestFrame.getHeaders()) { + httpRequest.addHeader(e.getKey(), e.getValue()); } // The Connection and Keep-Alive headers are no longer valid HttpHeaders.setKeepAlive(httpRequest, true); + // Transfer-Encoding header is not valid + httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + return httpRequest; } - private static HttpResponse createHttpResponse(SpdyHeaderBlock responseFrame) + private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeaderBlock responseFrame) throws Exception { // Create the first line of the response from the name/value pairs - HttpResponseStatus status = SpdyHeaders.getStatus(responseFrame); - HttpVersion version = SpdyHeaders.getVersion(responseFrame); - SpdyHeaders.removeStatus(responseFrame); - SpdyHeaders.removeVersion(responseFrame); + HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame); + HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame); + SpdyHeaders.removeStatus(spdyVersion, responseFrame); + SpdyHeaders.removeVersion(spdyVersion, responseFrame); HttpResponse httpResponse = new DefaultHttpResponse(version, status); for (Map.Entry e: responseFrame.getHeaders()) { httpResponse.addHeader(e.getKey(), e.getValue()); } - // Chunked encoding is no longer valid - List encodings = httpResponse.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING); - encodings.remove(HttpHeaders.Values.CHUNKED); - if (encodings.isEmpty()) { - httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); - } else { - httpResponse.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, encodings); - } - httpResponse.removeHeader(HttpHeaders.Names.TRAILER); - // The Connection and Keep-Alive headers are no longer valid HttpHeaders.setKeepAlive(httpResponse, true); + // Transfer-Encoding header is not valid + httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + httpResponse.removeHeader(HttpHeaders.Names.TRAILER); + return httpResponse; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java index f235482990..420cd1e410 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + import java.util.List; import java.util.Map; @@ -51,8 +53,8 @@ import io.netty.handler.codec.http.HttpResponse; * * {@code "X-SPDY-Priority"} * The priority value for this request. - * The priority should be between 0 and 3 inclusive. - * 0 represents the highest priority and 3 represents the lowest. + * The priority should be between 0 and 7 inclusive. + * 0 represents the highest priority and 7 represents the lowest. * This header is optional and defaults to 0. * * @@ -84,21 +86,32 @@ import io.netty.handler.codec.http.HttpResponse; * * * {@code "X-SPDY-Associated-To-Stream-ID"} - * The Stream-ID of the request that inititated this pushed resource. + * The Stream-ID of the request that initiated this pushed resource. * * * {@code "X-SPDY-Priority"} * The priority value for this resource. - * The priority should be between 0 and 3 inclusive. - * 0 represents the highest priority and 3 represents the lowest. + * The priority should be between 0 and 7 inclusive. + * 0 represents the highest priority and 7 represents the lowest. * This header is optional and defaults to 0. * * * {@code "X-SPDY-URL"} - * The full URL for the resource being pushed. + * The absolute path for the resource being pushed. * * * + *

Required Annotations

+ * + * SPDY requires that all Requests and Pushed Resources contain + * an HTTP "Host" header. + * + *

Optional Annotations

+ * + * Requests and Pushed Resources must contain a SPDY scheme header. + * This can be set via the {@code "X-SPDY-Scheme"} header but otherwise + * defaults to "https" as that is the most common SPDY deployment. + * *

Chunked Content

* * This encoder associates all {@link HttpChunk}s that it receives @@ -112,9 +125,20 @@ import io.netty.handler.codec.http.HttpResponse; */ public class SpdyHttpEncoder implements ChannelDownstreamHandler { + private final int spdyVersion; private volatile int currentStreamID; - public SpdyHttpEncoder() { + /** + * Creates a new instance. + * + * @param version the protocol version + */ + public SpdyHttpEncoder(int version) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + spdyVersion = version; } public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) @@ -228,15 +252,17 @@ public class SpdyHttpEncoder implements ChannelDownstreamHandler { throws Exception { boolean chunked = httpMessage.isChunked(); - // Get the Stream-ID, Associated-To-Stream-ID, Priority, and URL from the headers + // Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers int streamID = SpdyHttpHeaders.getStreamID(httpMessage); int associatedToStreamID = SpdyHttpHeaders.getAssociatedToStreamID(httpMessage); byte priority = SpdyHttpHeaders.getPriority(httpMessage); String URL = SpdyHttpHeaders.getUrl(httpMessage); + String scheme = SpdyHttpHeaders.getScheme(httpMessage); SpdyHttpHeaders.removeStreamID(httpMessage); SpdyHttpHeaders.removeAssociatedToStreamID(httpMessage); SpdyHttpHeaders.removePriority(httpMessage); SpdyHttpHeaders.removeUrl(httpMessage); + SpdyHttpHeaders.removeScheme(httpMessage); // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding // headers are not valid and MUST not be sent. @@ -246,24 +272,39 @@ public class SpdyHttpEncoder implements ChannelDownstreamHandler { httpMessage.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); - for (Map.Entry entry: httpMessage.getHeaders()) { - spdySynStreamFrame.addHeader(entry.getKey(), entry.getValue()); - } // Unfold the first line of the message into name/value pairs - SpdyHeaders.setVersion(spdySynStreamFrame, httpMessage.getProtocolVersion()); if (httpMessage instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpMessage; - SpdyHeaders.setMethod(spdySynStreamFrame, httpRequest.getMethod()); - SpdyHeaders.setUrl(spdySynStreamFrame, httpRequest.getUri()); + SpdyHeaders.setMethod(spdyVersion, spdySynStreamFrame, httpRequest.getMethod()); + SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, httpRequest.getUri()); + SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion()); } if (httpMessage instanceof HttpResponse) { HttpResponse httpResponse = (HttpResponse) httpMessage; - SpdyHeaders.setStatus(spdySynStreamFrame, httpResponse.getStatus()); - SpdyHeaders.setUrl(spdySynStreamFrame, URL); + SpdyHeaders.setStatus(spdyVersion, spdySynStreamFrame, httpResponse.getStatus()); + SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, URL); spdySynStreamFrame.setUnidirectional(true); } + // Replace the HTTP host header with the SPDY host header + if (spdyVersion >= 3) { + String host = HttpHeaders.getHost(httpMessage); + httpMessage.removeHeader(HttpHeaders.Names.HOST); + SpdyHeaders.setHost(spdySynStreamFrame, host); + } + + // Set the SPDY scheme header + if (scheme == null) { + scheme = "https"; + } + SpdyHeaders.setScheme(spdyVersion, spdySynStreamFrame, scheme); + + // Transfer the remaining HTTP headers + for (Map.Entry entry: httpMessage.getHeaders()) { + spdySynStreamFrame.addHeader(entry.getKey(), entry.getValue()); + } + if (chunked) { currentStreamID = streamID; spdySynStreamFrame.setLast(false); @@ -290,14 +331,16 @@ public class SpdyHttpEncoder implements ChannelDownstreamHandler { httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + + // Unfold the first line of the response into name/value pairs + SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, httpResponse.getStatus()); + SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, httpResponse.getProtocolVersion()); + + // Transfer the remaining HTTP headers for (Map.Entry entry: httpResponse.getHeaders()) { spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue()); } - // Unfold the first line of the repsonse into name/value pairs - SpdyHeaders.setStatus(spdySynReplyFrame, httpResponse.getStatus()); - SpdyHeaders.setVersion(spdySynReplyFrame, httpResponse.getProtocolVersion()); - if (chunked) { currentStreamID = streamID; spdySynReplyFrame.setLast(false); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java index ceb1af3d53..005a2e4c90 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java @@ -46,6 +46,10 @@ public final class SpdyHttpHeaders { * {@code "X-SPDY-URL"} */ public static final String URL = "X-SPDY-URL"; + /** + * {@code "X-SPDY-Scheme"} + */ + public static final String SCHEME = "X-SPDY-Scheme"; private Names() { super(); @@ -144,4 +148,25 @@ public final class SpdyHttpHeaders { public static void setUrl(HttpMessage message, String url) { message.setHeader(Names.URL, url); } + + /** + * Removes the {@code "X-SPDY-Scheme"} header. + */ + public static void removeScheme(HttpMessage message) { + message.removeHeader(Names.SCHEME); + } + + /** + * Returns the value of the {@code "X-SPDY-Scheme"} header. + */ + public static String getScheme(HttpMessage message) { + return message.getHeader(Names.SCHEME); + } + + /** + * Sets the {@code "X-SPDY-Scheme"} header. + */ + public static void setScheme(HttpMessage message, String scheme) { + message.setHeader(Names.URL, scheme); + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java index 4c68061ca0..c7457b6e7d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java @@ -15,11 +15,20 @@ */ package io.netty.handler.codec.spdy; +import java.util.Comparator; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import io.netty.channel.MessageEvent; final class SpdySession { + private static final SpdyProtocolException STREAM_CLOSED = new SpdyProtocolException("Stream closed"); + private final Map activeStreams = new ConcurrentHashMap(); @@ -38,17 +47,35 @@ final class SpdySession { return activeStreams.containsKey(new Integer(streamID)); } - public void acceptStream(int streamID, boolean remoteSideClosed, boolean localSideClosed) { + // Stream-IDs should be iterated in priority order + public Set getActiveStreams() { + TreeSet StreamIDs = new TreeSet(new PriorityComparator()); + StreamIDs.addAll(activeStreams.keySet()); + return StreamIDs; + } + + public void acceptStream( + int streamID, byte priority, boolean remoteSideClosed, boolean localSideClosed, + int sendWindowSize, int receiveWindowSize) { if (!remoteSideClosed || !localSideClosed) { - activeStreams.put(new Integer(streamID), - new StreamState(remoteSideClosed, localSideClosed)); + activeStreams.put( + new Integer(streamID), + new StreamState(priority, remoteSideClosed, localSideClosed, sendWindowSize, receiveWindowSize)); } return; } public void removeStream(int streamID) { - activeStreams.remove(new Integer(streamID)); - return; + Integer StreamID = new Integer(streamID); + StreamState state = activeStreams.get(StreamID); + activeStreams.remove(StreamID); + if (state != null) { + MessageEvent e = state.removePendingWrite(); + while (e != null) { + e.getFuture().setFailure(STREAM_CLOSED); + e = state.removePendingWrite(); + } + } } public boolean isRemoteSideClosed(int streamID) { @@ -83,6 +110,11 @@ final class SpdySession { } } + /* + * hasReceivedReply and receivedReply are only called from messageReceived + * no need to synchronize access to the StreamState + */ + public boolean hasReceivedReply(int streamID) { StreamState state = activeStreams.get(new Integer(streamID)); return (state != null) && state.hasReceivedReply(); @@ -95,15 +127,77 @@ final class SpdySession { } } + public int getSendWindowSize(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null ? state.getSendWindowSize() : -1; + } + + public int updateSendWindowSize(int streamID, int deltaWindowSize) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null ? state.updateSendWindowSize(deltaWindowSize) : -1; + } + + public int updateReceiveWindowSize(int streamID, int deltaWindowSize) { + StreamState state = activeStreams.get(new Integer(streamID)); + if (deltaWindowSize > 0) { + state.setReceiveWindowSizeLowerBound(0); + } + return state != null ? state.updateReceiveWindowSize(deltaWindowSize) : -1; + } + + public int getReceiveWindowSizeLowerBound(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null ? state.getReceiveWindowSizeLowerBound() : 0; + } + + public void updateAllReceiveWindowSizes(int deltaWindowSize) { + for (StreamState state: activeStreams.values()) { + state.updateReceiveWindowSize(deltaWindowSize); + if (deltaWindowSize < 0) { + state.setReceiveWindowSizeLowerBound(deltaWindowSize); + } + } + } + + public boolean putPendingWrite(int streamID, MessageEvent evt) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null && state.putPendingWrite(evt); + } + + public MessageEvent getPendingWrite(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null ? state.getPendingWrite() : null; + } + + public MessageEvent removePendingWrite(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return state != null ? state.removePendingWrite() : null; + } + private static final class StreamState { - private boolean remoteSideClosed; - private boolean localSideClosed; + private final byte priority; + private volatile boolean remoteSideClosed; + private volatile boolean localSideClosed; private boolean receivedReply; + private AtomicInteger sendWindowSize; + private AtomicInteger receiveWindowSize; + private volatile int receiveWindowSizeLowerBound; + private ConcurrentLinkedQueue pendingWriteQueue = + new ConcurrentLinkedQueue(); - public StreamState(boolean remoteSideClosed, boolean localSideClosed) { + public StreamState( + byte priority, boolean remoteSideClosed, boolean localSideClosed, + int sendWindowSize, int receiveWindowSize) { + this.priority = priority; this.remoteSideClosed = remoteSideClosed; this.localSideClosed = localSideClosed; + this.sendWindowSize = new AtomicInteger(sendWindowSize); + this.receiveWindowSize = new AtomicInteger(receiveWindowSize); + } + + public byte getPriority() { + return priority; } public boolean isRemoteSideClosed() { @@ -129,5 +223,50 @@ final class SpdySession { public void receivedReply() { receivedReply = true; } + + public int getSendWindowSize() { + return sendWindowSize.get(); + } + + public int updateSendWindowSize(int deltaWindowSize) { + return sendWindowSize.addAndGet(deltaWindowSize); + } + + public int updateReceiveWindowSize(int deltaWindowSize) { + return receiveWindowSize.addAndGet(deltaWindowSize); + } + + public int getReceiveWindowSizeLowerBound() { + return receiveWindowSizeLowerBound; + } + + public void setReceiveWindowSizeLowerBound(int receiveWindowSizeLowerBound) { + this.receiveWindowSizeLowerBound = receiveWindowSizeLowerBound; + } + + public boolean putPendingWrite(MessageEvent evt) { + return pendingWriteQueue.offer(evt); + } + + public MessageEvent getPendingWrite() { + return pendingWriteQueue.peek(); + } + + public MessageEvent removePendingWrite() { + return pendingWriteQueue.poll(); + } + } + + private final class PriorityComparator implements Comparator { + + public PriorityComparator() { + super(); + } + + public int compare(Integer id1, Integer id2) { + StreamState state1 = activeStreams.get(id1); + StreamState state2 = activeStreams.get(id2); + return state1.getPriority() - state2.getPriority(); + } } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java index b89d136cc5..c4850c0f04 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicInteger; @@ -46,6 +48,12 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler private volatile int localConcurrentStreams; private volatile int maxConcurrentStreams; + private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size + private volatile int initialSendWindowSize = DEFAULT_WINDOW_SIZE; + private volatile int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE; + + private final Object flowControlLock = new Object(); + private final AtomicInteger pings = new AtomicInteger(); private volatile boolean sentGoAwayFrame; @@ -54,18 +62,25 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler private volatile ChannelFuture closeSessionFuture; private final boolean server; + private final boolean flowControl; /** * Creates a new session handler. * - * @param server {@code true} if and only if this session handler should - * handle the server endpoint of the connection. - * {@code false} if and only if this session handler should - * handle the client endpoint of the connection. + * @param version the protocol version + * @param server {@code true} if and only if this session handler should + * handle the server endpoint of the connection. + * {@code false} if and only if this session handler should + * handle the client endpoint of the connection. */ - public SpdySessionHandler(boolean server) { + public SpdySessionHandler(int version, boolean server) { super(); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } this.server = server; + this.flowControl = version >= 3; } @Override @@ -78,41 +93,95 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler /* * SPDY Data frame processing requirements: * - * If an endpoint receives a data frame for a Stream-ID which does not exist, - * it must return a RST_STREAM with error code INVALID_STREAM for the Stream-ID. + * If an endpoint receives a data frame for a Stream-ID which is not open + * and the endpoint has not sent a GOAWAY frame, it must issue a stream error + * with the error code INVALID_STREAM for the Stream-ID. * * If an endpoint which created the stream receives a data frame before receiving - * a SYN_REPLY on that stream, it is a protocol error, and the receiver should - * close the connection immediately. + * a SYN_REPLY on that stream, it is a protocol error, and the recipient must + * issue a stream error with the status code PROTOCOL_ERROR for the Stream-ID. * * If an endpoint receives multiple data frames for invalid Stream-IDs, - * it may terminate the session. + * it may close the session. * * If an endpoint refuses a stream it must ignore any data frames for that stream. * - * If an endpoint receives data on a stream which has already been torn down, - * it must ignore the data received after the teardown. + * If an endpoint receives a data frame after the stream is half-closed from the + * sender, it must send a RST_STREAM frame with the status STREAM_ALREADY_CLOSED. + * + * If an endpoint receives a data frame after the stream is closed, it must send + * a RST_STREAM frame with the status PROTOCOL_ERROR. */ SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; int streamID = spdyDataFrame.getStreamID(); // Check if we received a data frame for a Stream-ID which is not open - if (spdySession.isRemoteSideClosed(streamID)) { - if (!sentGoAwayFrame) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + if (!spdySession.isActiveStream(streamID)) { + if (streamID <= lastGoodStreamID) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.PROTOCOL_ERROR); + } else if (!sentGoAwayFrame) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.INVALID_STREAM); } return; } + // Check if we received a data frame for a stream which is half-closed + if (spdySession.isRemoteSideClosed(streamID)) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.STREAM_ALREADY_CLOSED); + return; + } + // Check if we received a data frame before receiving a SYN_REPLY if (!isRemoteInitiatedID(streamID) && !spdySession.hasReceivedReply(streamID)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.PROTOCOL_ERROR); return; } + /* + * SPDY Data frame flow control processing requirements: + * + * Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame. + */ + + if (flowControl) { + // Update receive window size + int deltaWindowSize = -1 * spdyDataFrame.getData().readableBytes(); + int newWindowSize = spdySession.updateReceiveWindowSize(streamID, deltaWindowSize); + + // Window size can become negative if we sent a SETTINGS frame that reduces the + // size of the transfer window after the peer has written data frames. + // The value is bounded by the length that SETTINGS frame decrease the window. + // This difference is stored for the session when writing the SETTINGS frame + // and is cleared once we send a WINDOW_UPDATE frame. + if (newWindowSize < spdySession.getReceiveWindowSizeLowerBound(streamID)) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.FLOW_CONTROL_ERROR); + return; + } + + // Window size became negative due to sender writing frame before receiving SETTINGS + // Send data frames upstream in initialReceiveWindowSize chunks + if (newWindowSize < 0) { + while (spdyDataFrame.getData().readableBytes() > initialReceiveWindowSize) { + SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(streamID); + partialDataFrame.setData(spdyDataFrame.getData().readSlice(initialReceiveWindowSize)); + Channels.fireMessageReceived(ctx, partialDataFrame, e.getRemoteAddress()); + } + } + + // Send a WINDOW_UPDATE frame if less than half the window size remains + if (newWindowSize <= initialReceiveWindowSize / 2 && !spdyDataFrame.isLast()) { + deltaWindowSize = initialReceiveWindowSize - newWindowSize; + spdySession.updateReceiveWindowSize(streamID, deltaWindowSize); + SpdyWindowUpdateFrame spdyWindowUpdateFrame = + new DefaultSpdyWindowUpdateFrame(streamID, deltaWindowSize); + Channels.write( + ctx, Channels.future(e.getChannel()), spdyWindowUpdateFrame, e.getRemoteAddress()); + } + } + + // Close the remote side of the stream if this is the last frame if (spdyDataFrame.isLast()) { - // Close remote side of stream halfCloseStream(streamID, true); } @@ -121,11 +190,15 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler /* * SPDY SYN_STREAM frame processing requirements: * - * If an endpoint receives a SYN_STREAM with a Stream-ID that is not monotonically - * increasing, it must issue a session error with the status PROTOCOL_ERROR. + * If an endpoint receives a SYN_STREAM with a Stream-ID that is less than + * any previously received SYN_STREAM, it must issue a session error with + * the status PROTOCOL_ERROR. * * If an endpoint receives multiple SYN_STREAM frames with the same active * Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR. + * + * The recipient can reject a stream by sending a stream error with the + * status code REFUSED_STREAM. */ SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; @@ -135,21 +208,22 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler if (spdySynStreamFrame.isInvalid() || !isRemoteInitiatedID(streamID) || spdySession.isActiveStream(streamID)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.PROTOCOL_ERROR); return; } - // Stream-IDs must be monotonically increassing - if (streamID < lastGoodStreamID) { - issueSessionError(ctx, e.getChannel(), e.getRemoteAddress()); + // Stream-IDs must be monotonically increasing + if (streamID <= lastGoodStreamID) { + issueSessionError(ctx, e.getChannel(), e.getRemoteAddress(), SpdySessionStatus.PROTOCOL_ERROR); return; } // Try to accept the stream + byte priority = spdySynStreamFrame.getPriority(); boolean remoteSideClosed = spdySynStreamFrame.isLast(); boolean localSideClosed = spdySynStreamFrame.isUnidirectional(); - if (!acceptStream(streamID, remoteSideClosed, localSideClosed)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.REFUSED_STREAM); + if (!acceptStream(streamID, priority, remoteSideClosed, localSideClosed)) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.REFUSED_STREAM); return; } @@ -159,7 +233,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler * SPDY SYN_REPLY frame processing requirements: * * If an endpoint receives multiple SYN_REPLY frames for the same active Stream-ID - * it must issue a stream error with the status code PROTOCOL_ERROR. + * it must issue a stream error with the status code STREAM_IN_USE. */ SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; @@ -169,19 +243,20 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler if (spdySynReplyFrame.isInvalid() || isRemoteInitiatedID(streamID) || spdySession.isRemoteSideClosed(streamID)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.INVALID_STREAM); return; } // Check if we have received multiple frames for the same Stream-ID if (spdySession.hasReceivedReply(streamID)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.STREAM_IN_USE); return; } spdySession.receivedReply(streamID); + + // Close the remote side of the stream if this is the last frame if (spdySynReplyFrame.isLast()) { - // Close remote side of stream halfCloseStream(streamID, true); } @@ -190,8 +265,10 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler /* * SPDY RST_STREAM frame processing requirements: * - * After receiving a RST_STREAM on a stream, the receiver must not send additional - * frames on that stream. + * After receiving a RST_STREAM on a stream, the receiver must not send + * additional frames on that stream. + * + * An endpoint must not send a RST_STREAM in response to a RST_STREAM. */ SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; @@ -199,12 +276,29 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } else if (msg instanceof SpdySettingsFrame) { - /* - * Only concerned with MAX_CONCURRENT_STREAMS - */ - SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; - updateConcurrentStreams(spdySettingsFrame, true); + + int newConcurrentStreams = + spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); + if (newConcurrentStreams >= 0) { + updateConcurrentStreams(newConcurrentStreams, true); + } + + // Persistence flag are inconsistent with the use of SETTINGS to communicate + // the initial window size. Remove flags from the sender requesting that the + // value be persisted. Remove values that the sender indicates are persisted. + if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) { + spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); + } + spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false); + + if (flowControl) { + int newInitialWindowSize = + spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); + if (newInitialWindowSize >= 0) { + updateInitialSendWindowSize(newInitialWindowSize); + } + } } else if (msg instanceof SpdyPingFrame) { @@ -224,7 +318,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler return; } - // Note: only checks that there are outstanding pings since uniqueness is not inforced + // Note: only checks that there are outstanding pings since uniqueness is not enforced if (pings.get() == 0) { return; } @@ -241,14 +335,51 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler // Check if we received a valid HEADERS frame if (spdyHeadersFrame.isInvalid()) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.PROTOCOL_ERROR); return; } if (spdySession.isRemoteSideClosed(streamID)) { - issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.INVALID_STREAM); return; } + + // Close the remote side of the stream if this is the last frame + if (spdyHeadersFrame.isLast()) { + halfCloseStream(streamID, true); + } + + } else if (msg instanceof SpdyWindowUpdateFrame) { + + /* + * SPDY WINDOW_UPDATE frame processing requirements: + * + * Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31 + * must send a RST_STREAM with the status code FLOW_CONTROL_ERROR. + * + * Sender should ignore all WINDOW_UPDATE frames associated with a stream + * after sending the last frame for the stream. + */ + + if (flowControl) { + SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; + int streamID = spdyWindowUpdateFrame.getStreamID(); + int deltaWindowSize = spdyWindowUpdateFrame.getDeltaWindowSize(); + + // Ignore frames for half-closed streams + if (spdySession.isLocalSideClosed(streamID)) { + return; + } + + // Check for numerical overflow + if (spdySession.getSendWindowSize(streamID) > Integer.MAX_VALUE - deltaWindowSize) { + issueStreamError(ctx, e.getRemoteAddress(), streamID, SpdyStreamStatus.FLOW_CONTROL_ERROR); + return; + } + + updateSendWindowSize(ctx, streamID, deltaWindowSize); + } + return; } super.messageReceived(ctx, e); @@ -260,7 +391,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler Throwable cause = e.getCause(); if (cause instanceof SpdyProtocolException) { - issueSessionError(ctx, e.getChannel(), null); + issueSessionError(ctx, e.getChannel(), null, SpdySessionStatus.PROTOCOL_ERROR); } super.exceptionCaught(ctx, e); @@ -274,6 +405,13 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler case OPEN: case CONNECTED: case BOUND: + + /* + * SPDY connection requirements: + * + * When either endpoint closes the transport-level connection, + * it must first send a GOAWAY frame. + */ if (Boolean.FALSE.equals(e.getValue()) || e.getValue() == null) { sendGoAwayFrame(ctx, e); return; @@ -291,13 +429,85 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler if (msg instanceof SpdyDataFrame) { SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; - int streamID = spdyDataFrame.getStreamID(); + final int streamID = spdyDataFrame.getStreamID(); + // Frames must not be sent on half-closed streams if (spdySession.isLocalSideClosed(streamID)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } + /* + * SPDY Data frame flow control processing requirements: + * + * Sender must not send a data frame with data length greater + * than the transfer window size. + * + * After sending each data frame, the sender decrements its + * transfer window size by the amount of data transmitted. + * + * When the window size becomes less than or equal to 0, the + * sender must pause transmitting data frames. + */ + + if (flowControl) { + synchronized (flowControlLock) { + int dataLength = spdyDataFrame.getData().readableBytes(); + int sendWindowSize = spdySession.getSendWindowSize(streamID); + + if (sendWindowSize >= dataLength) { + // Window size is large enough to send entire data frame + spdySession.updateSendWindowSize(streamID, -1 * dataLength); + + // The transfer window size is pre-decremented when sending a data frame downstream. + // Close the stream on write failures that leaves the transfer window in a corrupt state. + final SocketAddress remoteAddress = e.getRemoteAddress(); + final ChannelHandlerContext context = ctx; + e.getFuture().addListener(new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + issueStreamError(context, remoteAddress, streamID, SpdyStreamStatus.INTERNAL_ERROR); + } + } + }); + + } else if (sendWindowSize > 0) { + // Stream is not stalled but we cannot send the entire frame + spdySession.updateSendWindowSize(streamID, -1 * sendWindowSize); + + // Create a partial data frame whose length is the current window size + SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(streamID); + partialDataFrame.setData(spdyDataFrame.getData().readSlice(sendWindowSize)); + + // Enqueue the remaining data (will be the first frame queued) + spdySession.putPendingWrite(streamID, e); + + ChannelFuture writeFuture = Channels.future(e.getChannel()); + + // The transfer window size is pre-decremented when sending a data frame downstream. + // Close the stream on write failures that leaves the transfer window in a corrupt state. + final SocketAddress remoteAddress = e.getRemoteAddress(); + final ChannelHandlerContext context = ctx; + e.getFuture().addListener(new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + issueStreamError(context, remoteAddress, streamID, SpdyStreamStatus.INTERNAL_ERROR); + } + } + }); + + Channels.write(ctx, writeFuture, partialDataFrame, remoteAddress); + return; + + } else { + // Stream is stalled -- enqueue Data frame and return + spdySession.putPendingWrite(streamID, e); + return; + } + } + } + + // Close the local side of the stream if this is the last frame if (spdyDataFrame.isLast()) { halfCloseStream(streamID, false); } @@ -305,9 +515,17 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } else if (msg instanceof SpdySynStreamFrame) { SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + int streamID = spdySynStreamFrame.getStreamID(); + + if (isRemoteInitiatedID(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + byte priority = spdySynStreamFrame.getPriority(); boolean remoteSideClosed = spdySynStreamFrame.isUnidirectional(); boolean localSideClosed = spdySynStreamFrame.isLast(); - if (!acceptStream(spdySynStreamFrame.getStreamID(), remoteSideClosed, localSideClosed)) { + if (!acceptStream(streamID, priority, remoteSideClosed, localSideClosed)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } @@ -317,11 +535,13 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; int streamID = spdySynReplyFrame.getStreamID(); + // Frames must not be sent on half-closed streams if (!isRemoteInitiatedID(streamID) || spdySession.isLocalSideClosed(streamID)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } + // Close the local side of the stream if this is the last frame if (spdySynReplyFrame.isLast()) { halfCloseStream(streamID, false); } @@ -334,7 +554,28 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } else if (msg instanceof SpdySettingsFrame) { SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; - updateConcurrentStreams(spdySettingsFrame, false); + + int newConcurrentStreams = + spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); + if (newConcurrentStreams >= 0) { + updateConcurrentStreams(newConcurrentStreams, false); + } + + // Persistence flag are inconsistent with the use of SETTINGS to communicate + // the initial window size. Remove flags from the sender requesting that the + // value be persisted. Remove values that the sender indicates are persisted. + if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) { + spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); + } + spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false); + + if (flowControl) { + int newInitialWindowSize = + spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); + if (newInitialWindowSize >= 0) { + updateInitialReceiveWindowSize(newInitialWindowSize); + } + } } else if (msg instanceof SpdyPingFrame) { @@ -348,7 +589,8 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } else if (msg instanceof SpdyGoAwayFrame) { - // Should send a CLOSE ChannelStateEvent + // Why is this being sent? Intercept it and fail the write. + // Should have sent a CLOSE ChannelStateEvent e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; @@ -357,34 +599,65 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; int streamID = spdyHeadersFrame.getStreamID(); + // Frames must not be sent on half-closed streams if (spdySession.isLocalSideClosed(streamID)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } + + // Close the local side of the stream if this is the last frame + if (spdyHeadersFrame.isLast()) { + halfCloseStream(streamID, false); + } + + } else if (msg instanceof SpdyWindowUpdateFrame) { + + // Why is this being sent? Intercept it and fail the write. + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; } ctx.sendDownstream(evt); } /* - * Error Handling + * SPDY Session Error Handling: + * + * When a session error occurs, the endpoint encountering the error must first + * send a GOAWAY frame with the Stream-ID of the most recently received stream + * from the remote endpoint, and the error code for why the session is terminating. + * + * After sending the GOAWAY frame, the endpoint must close the TCP connection. */ - private void issueSessionError( - ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress, SpdySessionStatus status) { - ChannelFuture future = sendGoAwayFrame(ctx, channel, remoteAddress); + ChannelFuture future = sendGoAwayFrame(ctx, channel, remoteAddress, status); future.addListener(ChannelFutureListener.CLOSE); } - // Send a RST_STREAM frame in response to an incoming MessageEvent - // Only called in the upstream direction + /* + * SPDY Stream Error Handling: + * + * Upon a stream error, the endpoint must send a RST_STREAM frame which contains + * the Stream-ID for the stream where the error occurred and the error status which + * caused the error. + * + * After sending the RST_STREAM, the stream is closed to the sending endpoint. + * + * Note: this is only called by the worker thread + */ private void issueStreamError( - ChannelHandlerContext ctx, MessageEvent e, int streamID, SpdyStreamStatus status) { + ChannelHandlerContext ctx, SocketAddress remoteAddress, int streamID, SpdyStreamStatus status) { + boolean fireMessageReceived = !spdySession.isRemoteSideClosed(streamID); removeStream(streamID); + SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamID, status); - Channels.write(ctx, Channels.future(e.getChannel()), spdyRstStreamFrame, e.getRemoteAddress()); + Channels.write(ctx, Channels.future(ctx.getChannel()), spdyRstStreamFrame, remoteAddress); + if (fireMessageReceived) { + Channels.fireMessageReceived(ctx, spdyRstStreamFrame, remoteAddress); + } } /* @@ -396,8 +669,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler return server && !serverID || !server && serverID; } - private synchronized void updateConcurrentStreams(SpdySettingsFrame settings, boolean remote) { - int newConcurrentStreams = settings.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); + private void updateConcurrentStreams(int newConcurrentStreams, boolean remote) { if (remote) { remoteConcurrentStreams = newConcurrentStreams; } else { @@ -422,18 +694,37 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } } - // need to synchronize accesses to sentGoAwayFrame and lastGoodStreamID + // need to synchronize to prevent new streams from being created while updating active streams + private synchronized void updateInitialSendWindowSize(int newInitialWindowSize) { + int deltaWindowSize = newInitialWindowSize - initialSendWindowSize; + initialSendWindowSize = newInitialWindowSize; + for (Integer StreamID: spdySession.getActiveStreams()) { + spdySession.updateSendWindowSize(StreamID.intValue(), deltaWindowSize); + } + } + + // need to synchronize to prevent new streams from being created while updating active streams + private synchronized void updateInitialReceiveWindowSize(int newInitialWindowSize) { + int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize; + initialReceiveWindowSize = newInitialWindowSize; + spdySession.updateAllReceiveWindowSizes(deltaWindowSize); + } + + // need to synchronize accesses to sentGoAwayFrame, lastGoodStreamID, and initial window sizes private synchronized boolean acceptStream( - int streamID, boolean remoteSideClosed, boolean localSideClosed) { + int streamID, byte priority, boolean remoteSideClosed, boolean localSideClosed) { // Cannot initiate any new streams after receiving or sending GOAWAY if (receivedGoAwayFrame || sentGoAwayFrame) { return false; } + + int maxConcurrentStreams = this.maxConcurrentStreams; // read volatile once if (maxConcurrentStreams != 0 && spdySession.numActiveStreams() >= maxConcurrentStreams) { return false; } - spdySession.acceptStream(streamID, remoteSideClosed, localSideClosed); + spdySession.acceptStream( + streamID, priority, remoteSideClosed, localSideClosed, initialSendWindowSize, initialReceiveWindowSize); if (isRemoteInitiatedID(streamID)) { lastGoodStreamID = streamID; } @@ -458,6 +749,74 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } } + private void updateSendWindowSize(ChannelHandlerContext ctx, final int streamID, int deltaWindowSize) { + synchronized (flowControlLock) { + int newWindowSize = spdySession.updateSendWindowSize(streamID, deltaWindowSize); + + while (newWindowSize > 0) { + // Check if we have unblocked a stalled stream + MessageEvent e = spdySession.getPendingWrite(streamID); + if (e == null) { + break; + } + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) e.getMessage(); + int dataFrameSize = spdyDataFrame.getData().readableBytes(); + + if (newWindowSize >= dataFrameSize) { + // Window size is large enough to send entire data frame + spdySession.removePendingWrite(streamID); + newWindowSize = spdySession.updateSendWindowSize(streamID, -1 * dataFrameSize); + + // The transfer window size is pre-decremented when sending a data frame downstream. + // Close the stream on write failures that leaves the transfer window in a corrupt state. + final SocketAddress remoteAddress = e.getRemoteAddress(); + final ChannelHandlerContext context = ctx; + e.getFuture().addListener(new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + issueStreamError(context, remoteAddress, streamID, SpdyStreamStatus.INTERNAL_ERROR); + } + } + }); + + // Close the local side of the stream if this is the last frame + if (spdyDataFrame.isLast()) { + halfCloseStream(streamID, false); + } + + Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress()); + + } else { + // We can send a partial frame + spdySession.updateSendWindowSize(streamID, -1 * newWindowSize); + + // Create a partial data frame whose length is the current window size + SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(streamID); + partialDataFrame.setData(spdyDataFrame.getData().readSlice(newWindowSize)); + + ChannelFuture writeFuture = Channels.future(e.getChannel()); + + // The transfer window size is pre-decremented when sending a data frame downstream. + // Close the stream on write failures that leaves the transfer window in a corrupt state. + final SocketAddress remoteAddress = e.getRemoteAddress(); + final ChannelHandlerContext context = ctx; + e.getFuture().addListener(new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + issueStreamError(context, remoteAddress, streamID, SpdyStreamStatus.INTERNAL_ERROR); + } + } + }); + + Channels.write(ctx, writeFuture, partialDataFrame, remoteAddress); + + newWindowSize = 0; + } + } + } + } + private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelStateEvent e) { // Avoid NotYetConnectedException if (!e.getChannel().isConnected()) { @@ -465,7 +824,7 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler return; } - ChannelFuture future = sendGoAwayFrame(ctx, e.getChannel(), null); + ChannelFuture future = sendGoAwayFrame(ctx, e.getChannel(), null, SpdySessionStatus.OK); if (spdySession.noActiveStreams()) { future.addListener(new ClosingChannelFutureListener(ctx, e)); } else { @@ -475,18 +834,18 @@ public class SpdySessionHandler extends SimpleChannelUpstreamHandler } private synchronized ChannelFuture sendGoAwayFrame( - ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress, SpdySessionStatus status) { if (!sentGoAwayFrame) { sentGoAwayFrame = true; + SpdyGoAwayFrame spdyGoAwayFrame = new DefaultSpdyGoAwayFrame(lastGoodStreamID, status); ChannelFuture future = Channels.future(channel); - Channels.write(ctx, future, new DefaultSpdyGoAwayFrame(lastGoodStreamID), remoteAddress); + Channels.write(ctx, future, spdyGoAwayFrame, remoteAddress); return future; } return Channels.succeededFuture(channel); } private static final class ClosingChannelFutureListener implements ChannelFutureListener { - private final ChannelHandlerContext ctx; private final ChannelStateEvent e; diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java new file mode 100644 index 0000000000..c929c0d5e7 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.spdy; + +/** + * The SPDY session status code and its description. + * @apiviz.exclude + */ +public class SpdySessionStatus implements Comparable { + + /** + * 0 OK + */ + public static final SpdySessionStatus OK = + new SpdySessionStatus(0, "OK"); + + /** + * 1 Protocol Error + */ + public static final SpdySessionStatus PROTOCOL_ERROR = + new SpdySessionStatus(1, "PROTOCOL_ERROR"); + + /** + * 11 Internal Error + */ + public static final SpdySessionStatus INTERNAL_ERROR = + new SpdySessionStatus(11, "INTERNAL_ERROR"); + + /** + * Returns the {@link SpdySessionStatus} represented by the specified code. + * If the specified code is a defined SPDY status code, a cached instance + * will be returned. Otherwise, a new instance will be returned. + */ + public static SpdySessionStatus valueOf(int code) { + switch (code) { + case 0: + return OK; + case 1: + return PROTOCOL_ERROR; + case 11: + return INTERNAL_ERROR; + } + + return new SpdySessionStatus(code, "UNKNOWN (" + code + ')'); + } + + private final int code; + + private final String statusPhrase; + + /** + * Creates a new instance with the specified {@code code} and its + * {@code statusPhrase}. + */ + public SpdySessionStatus(int code, String statusPhrase) { + if (statusPhrase == null) { + throw new NullPointerException("statusPhrase"); + } + + this.code = code; + this.statusPhrase = statusPhrase; + } + + /** + * Returns the code of this status. + */ + public int getCode() { + return code; + } + + /** + * Returns the status phrase of this status. + */ + public String getStatusPhrase() { + return statusPhrase; + } + + @Override + public int hashCode() { + return getCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SpdySessionStatus)) { + return false; + } + + return getCode() == ((SpdySessionStatus) o).getCode(); + } + + @Override + public String toString() { + return getStatusPhrase(); + } + + public int compareTo(SpdySessionStatus o) { + return getCode() - o.getCode(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java index 986db39542..96d237ceb4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java @@ -22,13 +22,14 @@ import java.util.Set; */ public interface SpdySettingsFrame { - int SETTINGS_UPLOAD_BANDWIDTH = 1; - int SETTINGS_DOWNLOAD_BANDWIDTH = 2; - int SETTINGS_ROUND_TRIP_TIME = 3; - int SETTINGS_MAX_CONCURRENT_STREAMS = 4; - int SETTINGS_CURRENT_CWND = 5; - int SETTINGS_DOWNLOAD_RETRANS_RATE = 6; - int SETTINGS_INITIAL_WINDOW_SIZE = 7; + int SETTINGS_UPLOAD_BANDWIDTH = 1; + int SETTINGS_DOWNLOAD_BANDWIDTH = 2; + int SETTINGS_ROUND_TRIP_TIME = 3; + int SETTINGS_MAX_CONCURRENT_STREAMS = 4; + int SETTINGS_CURRENT_CWND = 5; + int SETTINGS_DOWNLOAD_RETRANS_RATE = 6; + int SETTINGS_INITIAL_WINDOW_SIZE = 7; + int SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE = 8; /** * Returns a {@code Set} of the setting IDs. @@ -49,7 +50,7 @@ public interface SpdySettingsFrame { /** * Sets the value of the setting ID. - * The ID must be positive and cannot exceeed 16777215. + * The ID must be positive and cannot exceed 16777215. */ void setValue(int ID, int value); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java index 57689f0098..feeb816022 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java @@ -63,6 +63,30 @@ public class SpdyStreamStatus implements Comparable { public static final SpdyStreamStatus FLOW_CONTROL_ERROR = new SpdyStreamStatus(7, "FLOW_CONTROL_ERROR"); + /** + * 8 Stream In Use + */ + public static final SpdyStreamStatus STREAM_IN_USE = + new SpdyStreamStatus(8, "STREAM_IN_USE"); + + /** + * 9 Stream Already Closed + */ + public static final SpdyStreamStatus STREAM_ALREADY_CLOSED = + new SpdyStreamStatus(9, "STREAM_ALREADY_CLOSED"); + + /** + * 10 Invalid Credentials + */ + public static final SpdyStreamStatus INVALID_CREDENTIALS = + new SpdyStreamStatus(10, "INVALID_CREDENTIALS"); + + /** + * 11 Frame Too Large + */ + public static final SpdyStreamStatus FRAME_TOO_LARGE = + new SpdyStreamStatus(11, "FRAME_TOO_LARGE"); + /** * Returns the {@link SpdyStreamStatus} represented by the specified code. * If the specified code is a defined SPDY status code, a cached instance @@ -89,6 +113,14 @@ public class SpdyStreamStatus implements Comparable { return INTERNAL_ERROR; case 7: return FLOW_CONTROL_ERROR; + case 8: + return STREAM_IN_USE; + case 9: + return STREAM_ALREADY_CLOSED; + case 10: + return INVALID_CREDENTIALS; + case 11: + return FRAME_TOO_LARGE; } return new SpdyStreamStatus(code, "UNKNOWN (" + code + ')'); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java index b69bcf3b16..4e2e069a91 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java @@ -48,7 +48,7 @@ public interface SpdySynStreamFrame extends SpdyHeaderBlock { /** * Sets the priority of the stream. - * The priority must be between 0 and 3 inclusive. + * The priority must be between 0 and 7 inclusive. */ void setPriority(byte priority); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java index 18f9712987..4935cf2ada 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.handler.codec.spdy; +package io.netty.handler.codec.spdy; /** * A SPDY Protocol WINDOW_UPDATE Control Frame diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java index 3e08e7d89f..8c6e67b723 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java @@ -17,6 +17,8 @@ package io.netty.handler.codec.spdy; import static org.junit.Assert.*; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -47,15 +49,17 @@ import org.junit.Test; public abstract class AbstractSocketSpdyEchoTest { private static final Random random = new Random(); - static final ChannelBuffer frames = ChannelBuffers.buffer(1176); static final int ignoredBytes = 20; private static ExecutorService executor; - static { + private static ChannelBuffer createFrames(int version) { + int length = version < 3 ? 1176 : 1174; + ChannelBuffer frames = ChannelBuffers.buffer(length); + // SPDY UNKNOWN Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(0xFFFF); frames.writeByte(0xFF); frames.writeMedium(4); @@ -63,7 +67,7 @@ public abstract class AbstractSocketSpdyEchoTest { // SPDY NOOP Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(5); frames.writeInt(0); @@ -77,27 +81,39 @@ public abstract class AbstractSocketSpdyEchoTest { // SPDY SYN_STREAM Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(1); frames.writeByte(0x03); - frames.writeMedium(12); + if (version < 3) { + frames.writeMedium(12); + } else { + frames.writeMedium(10); + } frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeInt(random.nextInt() & 0x7FFFFFFF); frames.writeShort(0x8000); - frames.writeShort(0); + if (version < 3) { + frames.writeShort(0); + } // SPDY SYN_REPLY Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(2); frames.writeByte(0x01); - frames.writeMedium(8); + if (version < 3) { + frames.writeMedium(8); + } else { + frames.writeMedium(4); + } frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); - frames.writeInt(0); + if (version < 3) { + frames.writeInt(0); + } // SPDY RST_STREAM Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(3); frames.writeInt(8); frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); @@ -105,43 +121,58 @@ public abstract class AbstractSocketSpdyEchoTest { // SPDY SETTINGS Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(4); frames.writeByte(0x01); frames.writeMedium(12); frames.writeInt(1); - frames.writeMedium(random.nextInt()); - frames.writeByte(0x03); + if (version < 3) { + frames.writeMedium(random.nextInt()); + frames.writeByte(0x03); + } else { + frames.writeByte(0x03); + frames.writeMedium(random.nextInt()); + } frames.writeInt(random.nextInt()); // SPDY PING Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(6); frames.writeInt(4); frames.writeInt(random.nextInt()); // SPDY GOAWAY Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(7); - frames.writeInt(4); + if (version < 3) { + frames.writeInt(4); + } else { + frames.writeInt(8); + } frames.writeInt(random.nextInt() & 0x7FFFFFFF); + if (version >= 3) { + frames.writeInt(random.nextInt() | 0x01); + } // SPDY HEADERS Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(8); - frames.writeInt(4); + frames.writeByte(0x01); + frames.writeMedium(4); frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); // SPDY WINDOW_UPDATE Frame frames.writeByte(0x80); - frames.writeByte(2); + frames.writeByte(version); frames.writeShort(9); frames.writeInt(8); frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + + return frames; } @BeforeClass @@ -159,14 +190,22 @@ public abstract class AbstractSocketSpdyEchoTest { @Test(timeout = 10000) public void testSpdyEcho() throws Throwable { + for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version ++) { + testSpdyEcho(version); + } + } + + private void testSpdyEcho(int version) throws Throwable { ServerBootstrap sb = new ServerBootstrap(newServerSocketChannelFactory(executor)); ClientBootstrap cb = new ClientBootstrap(newClientSocketChannelFactory(executor)); - EchoHandler sh = new EchoHandler(true); - EchoHandler ch = new EchoHandler(false); + ChannelBuffer frames = createFrames(version); - sb.getPipeline().addLast("decoder", new SpdyFrameDecoder()); - sb.getPipeline().addLast("encoder", new SpdyFrameEncoder()); + EchoHandler sh = new EchoHandler(frames, true); + EchoHandler ch = new EchoHandler(frames, false); + + sb.getPipeline().addLast("decoder", new SpdyFrameDecoder(version)); + sb.getPipeline().addLast("encoder", new SpdyFrameEncoder(version)); sb.getPipeline().addLast("handler", sh); cb.getPipeline().addLast("handler", ch); @@ -216,11 +255,13 @@ public abstract class AbstractSocketSpdyEchoTest { private class EchoHandler extends SimpleChannelUpstreamHandler { volatile Channel channel; final AtomicReference exception = new AtomicReference(); + final ChannelBuffer frames; volatile int counter; final boolean server; - EchoHandler(boolean server) { + EchoHandler(ChannelBuffer frames, boolean server) { super(); + this.frames = frames; this.server = server; } diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java index 893f0baed1..c4277f6f79 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java @@ -95,10 +95,10 @@ public class SpdySessionHandlerTest { assertHeaderBlock(spdyHeadersFrame, headers); } - private void testSpdySessionHandler(boolean server) { + private void testSpdySessionHandler(int version, boolean server) { DecoderEmbedder sessionHandler = new DecoderEmbedder( - new SpdySessionHandler(server), new EchoHandler(closeSignal, server)); + new SpdySessionHandler(version, server), new EchoHandler(closeSignal, server)); sessionHandler.pollAll(); int localStreamID = server ? 1 : 2; @@ -132,7 +132,7 @@ public class SpdySessionHandlerTest { sessionHandler.offer(new DefaultSpdySynReplyFrame(remoteStreamID)); Assert.assertNull(sessionHandler.peek()); sessionHandler.offer(new DefaultSpdySynReplyFrame(remoteStreamID)); - assertRstStream(sessionHandler.poll(), remoteStreamID, SpdyStreamStatus.PROTOCOL_ERROR); + assertRstStream(sessionHandler.poll(), remoteStreamID, SpdyStreamStatus.STREAM_IN_USE); Assert.assertNull(sessionHandler.peek()); remoteStreamID += 2; @@ -252,12 +252,12 @@ public class SpdySessionHandlerTest { @Test public void testSpdyClientSessionHandler() { - testSpdySessionHandler(false); + testSpdySessionHandler(2, false); } @Test public void testSpdyServerSessionHandler() { - testSpdySessionHandler(true); + testSpdySessionHandler(2, true); } // Echo Handler opens 4 half-closed streams on session connection