From 1a28d6119fb5d2e77caae8674540f62a6b9043e1 Mon Sep 17 00:00:00 2001 From: Jeff Pinner Date: Wed, 23 May 2012 08:54:11 -0700 Subject: [PATCH] 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