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 e0b40bd9a4..2405b512e4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -15,12 +15,38 @@ */ package io.netty.handler.codec.spdy; -import io.netty.channel.CombinedChannelDuplexHandler; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.UnsupportedMessageTypeException; + +import java.net.SocketAddress; +import java.util.List; /** - * A combination of {@link SpdyFrameDecoder} and {@link SpdyFrameEncoder}. + * A {@link ChannelHandler} that encodes and decodes SPDY Frames. */ -public final class SpdyFrameCodec extends CombinedChannelDuplexHandler { +public final class SpdyFrameCodec extends ByteToMessageDecoder + implements SpdyFrameDecoderDelegate, ChannelOutboundHandler { + + private static final SpdyProtocolException INVALID_FRAME = + new SpdyProtocolException("Received invalid frame"); + + private final SpdyFrameDecoder spdyFrameDecoder; + private final SpdyFrameEncoder spdyFrameEncoder; + private final SpdyHeaderBlockDecoder spdyHeaderBlockDecoder; + private final SpdyHeaderBlockEncoder spdyHeaderBlockEncoder; + + private SpdyHeadersFrame spdyHeadersFrame; + private SpdySettingsFrame spdySettingsFrame; + + private ChannelHandlerContext ctx; + /** * Creates a new instance with the specified {@code version} and * the default decoder and encoder options @@ -38,8 +64,289 @@ public final class SpdyFrameCodec extends CombinedChannelDuplexHandler out) throws Exception { + spdyFrameDecoder.decode(in); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ByteBuf frame; + + if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + frame = spdyFrameEncoder.encodeDataFrame( + ctx.alloc(), + spdyDataFrame.getStreamId(), + spdyDataFrame.isLast(), + spdyDataFrame.content() + ); + spdyDataFrame.release(); + ctx.write(frame, promise); + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + ByteBuf headerBlock = spdyHeaderBlockEncoder.encode(spdySynStreamFrame); + try { + frame = spdyFrameEncoder.encodeSynStreamFrame( + ctx.alloc(), + spdySynStreamFrame.getStreamId(), + spdySynStreamFrame.getAssociatedToStreamId(), + spdySynStreamFrame.getPriority(), + spdySynStreamFrame.isLast(), + spdySynStreamFrame.isUnidirectional(), + headerBlock + ); + } finally { + headerBlock.release(); + } + ctx.write(frame, promise); + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + ByteBuf headerBlock = spdyHeaderBlockEncoder.encode(spdySynReplyFrame); + try { + frame = spdyFrameEncoder.encodeSynReplyFrame( + ctx.alloc(), + spdySynReplyFrame.getStreamId(), + spdySynReplyFrame.isLast(), + headerBlock + ); + } finally { + headerBlock.release(); + } + ctx.write(frame, promise); + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + frame = spdyFrameEncoder.encodeRstStreamFrame( + ctx.alloc(), + spdyRstStreamFrame.getStreamId(), + spdyRstStreamFrame.getStatus().getCode() + ); + ctx.write(frame, promise); + + } else if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + frame = spdyFrameEncoder.encodeSettingsFrame( + ctx.alloc(), + spdySettingsFrame + ); + ctx.write(frame, promise); + + } else if (msg instanceof SpdyPingFrame) { + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + frame = spdyFrameEncoder.encodePingFrame( + ctx.alloc(), + spdyPingFrame.getId() + ); + ctx.write(frame, promise); + + } else if (msg instanceof SpdyGoAwayFrame) { + + SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + frame = spdyFrameEncoder.encodeGoAwayFrame( + ctx.alloc(), + spdyGoAwayFrame.getLastGoodStreamId(), + spdyGoAwayFrame.getStatus().getCode() + ); + ctx.write(frame, promise); + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + ByteBuf headerBlock = spdyHeaderBlockEncoder.encode(spdyHeadersFrame); + try { + frame = spdyFrameEncoder.encodeHeadersFrame( + ctx.alloc(), + spdyHeadersFrame.getStreamId(), + spdyHeadersFrame.isLast(), + headerBlock + ); + } finally { + headerBlock.release(); + } + ctx.write(frame, promise); + + } else if (msg instanceof SpdyWindowUpdateFrame) { + + SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; + frame = spdyFrameEncoder.encodeWindowUpdateFrame( + ctx.alloc(), + spdyWindowUpdateFrame.getStreamId(), + spdyWindowUpdateFrame.getDeltaWindowSize() + ); + ctx.write(frame, promise); + } else { + throw new UnsupportedMessageTypeException(msg); + } + } + + @Override + public void readDataFrame(int streamId, boolean last, ByteBuf data) { + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamId, data); + spdyDataFrame.setLast(last); + ctx.fireChannelRead(spdyDataFrame); + } + + @Override + public void readSynStreamFrame( + int streamId, int associatedToStreamId, byte priority, boolean last, boolean unidirectional) { + SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(streamId, associatedToStreamId, priority); + spdySynStreamFrame.setLast(last); + spdySynStreamFrame.setUnidirectional(unidirectional); + spdyHeadersFrame = spdySynStreamFrame; + } + + @Override + public void readSynReplyFrame(int streamId, boolean last) { + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); + spdySynReplyFrame.setLast(last); + spdyHeadersFrame = spdySynReplyFrame; + } + + @Override + public void readRstStreamFrame(int streamId, int statusCode) { + SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, statusCode); + ctx.fireChannelRead(spdyRstStreamFrame); + } + + @Override + public void readSettingsFrame(boolean clearPersisted) { + spdySettingsFrame = new DefaultSpdySettingsFrame(); + spdySettingsFrame.setClearPreviouslyPersistedSettings(clearPersisted); + } + + @Override + public void readSetting(int id, int value, boolean persistValue, boolean persisted) { + spdySettingsFrame.setValue(id, value, persistValue, persisted); + } + + @Override + public void readSettingsEnd() { + Object frame = spdySettingsFrame; + spdySettingsFrame = null; + ctx.fireChannelRead(frame); + } + + @Override + public void readPingFrame(int id) { + SpdyPingFrame spdyPingFrame = new DefaultSpdyPingFrame(id); + ctx.fireChannelRead(spdyPingFrame); + } + + @Override + public void readGoAwayFrame(int lastGoodStreamId, int statusCode) { + SpdyGoAwayFrame spdyGoAwayFrame = new DefaultSpdyGoAwayFrame(lastGoodStreamId, statusCode); + ctx.fireChannelRead(spdyGoAwayFrame); + } + + @Override + public void readHeadersFrame(int streamId, boolean last) { + spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamId); + spdyHeadersFrame.setLast(last); + } + + @Override + public void readWindowUpdateFrame(int streamId, int deltaWindowSize) { + SpdyWindowUpdateFrame spdyWindowUpdateFrame = new DefaultSpdyWindowUpdateFrame(streamId, deltaWindowSize); + ctx.fireChannelRead(spdyWindowUpdateFrame); + } + + @Override + public void readHeaderBlock(ByteBuf headerBlock) { + try { + spdyHeaderBlockDecoder.decode(headerBlock, spdyHeadersFrame); + } catch (Exception e) { + ctx.fireExceptionCaught(e); + } + } + + @Override + public void readHeaderBlockEnd() { + Object frame = null; + try { + spdyHeaderBlockDecoder.endHeaderBlock(spdyHeadersFrame); + frame = spdyHeadersFrame; + spdyHeadersFrame = null; + } catch (Exception e) { + ctx.fireExceptionCaught(e); + } + if (frame != null) { + ctx.fireChannelRead(frame); + } + } + + @Override + public void readFrameError(String message) { + ctx.fireExceptionCaught(INVALID_FRAME); } } 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 c44fc277ee..e0d1112813 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -39,511 +39,427 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedInt; import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedMedium; import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedShort; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ByteToMessageDecoder; - -import java.util.List; +import io.netty.buffer.Unpooled; /** * Decodes {@link ByteBuf}s into SPDY Frames. */ -public class SpdyFrameDecoder extends ByteToMessageDecoder { - - private static final SpdyProtocolException INVALID_FRAME = - new SpdyProtocolException("Received invalid frame"); +public class SpdyFrameDecoder { private final int spdyVersion; private final int maxChunkSize; - private final SpdyHeaderBlockDecoder headerBlockDecoder; + private final SpdyFrameDecoderDelegate delegate; private State state; - private SpdySettingsFrame spdySettingsFrame; - private SpdyHeadersFrame spdyHeadersFrame; // SPDY common header fields private byte flags; private int length; - private int version; - private int type; private int streamId; + private int numSettings; + private enum State { READ_COMMON_HEADER, - READ_CONTROL_FRAME, - READ_SETTINGS_FRAME, - READ_HEADER_BLOCK_FRAME, - READ_HEADER_BLOCK, READ_DATA_FRAME, + READ_SYN_STREAM_FRAME, + READ_SYN_REPLY_FRAME, + READ_RST_STREAM_FRAME, + READ_SETTINGS_FRAME, + READ_SETTING, + READ_PING_FRAME, + READ_GOAWAY_FRAME, + READ_HEADERS_FRAME, + READ_WINDOW_UPDATE_FRAME, + READ_HEADER_BLOCK, DISCARD_FRAME, FRAME_ERROR } /** - * Creates a new instance with the specified {@code version} and the default - * {@code maxChunkSize (8192)} and {@code maxHeaderSize (16384)}. + * Creates a new instance with the specified {@code version} + * and the default {@code maxChunkSize (8192)}. */ - public SpdyFrameDecoder(SpdyVersion version) { - this(version, 8192, 16384); + public SpdyFrameDecoder(SpdyVersion spdyVersion, SpdyFrameDecoderDelegate delegate) { + this(spdyVersion, delegate, 8192); } /** * Creates a new instance with the specified parameters. */ - public SpdyFrameDecoder(SpdyVersion version, int maxChunkSize, int maxHeaderSize) { - this(version, maxChunkSize, SpdyHeaderBlockDecoder.newInstance(version, maxHeaderSize)); - } - - protected SpdyFrameDecoder( - SpdyVersion version, int maxChunkSize, SpdyHeaderBlockDecoder headerBlockDecoder) { - if (version == null) { - throw new NullPointerException("version"); + public SpdyFrameDecoder(SpdyVersion spdyVersion, SpdyFrameDecoderDelegate delegate, int maxChunkSize) { + if (spdyVersion == null) { + throw new NullPointerException("spdyVersion"); + } + if (delegate == null) { + throw new NullPointerException("delegate"); } if (maxChunkSize <= 0) { throw new IllegalArgumentException( "maxChunkSize must be a positive integer: " + maxChunkSize); } - spdyVersion = version.getVersion(); + this.spdyVersion = spdyVersion.getVersion(); + this.delegate = delegate; this.maxChunkSize = maxChunkSize; - this.headerBlockDecoder = headerBlockDecoder; state = State.READ_COMMON_HEADER; } - @Override - public void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - try { - decode(ctx, in, out); - } finally { - headerBlockDecoder.end(); - } - } + public void decode(ByteBuf buffer) { + boolean last; + int statusCode; - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List out) throws Exception { - switch(state) { - case READ_COMMON_HEADER: - state = readCommonHeader(buffer); - if (state == State.FRAME_ERROR) { - if (version != spdyVersion) { - fireProtocolException(ctx, "Unsupported version: " + version); - } else { - fireInvalidFrameException(ctx); - } - } - - // FrameDecoders must consume data when producing frames - // All length 0 frames must be generated now - if (length == 0) { - if (state == State.READ_DATA_FRAME) { - SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamId); - spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); - state = State.READ_COMMON_HEADER; - out.add(spdyDataFrame); - return; - } - // There are no length 0 control frames - state = State.READ_COMMON_HEADER; - } - - return; - - case READ_CONTROL_FRAME: - try { - Object frame = readControlFrame(buffer); - if (frame != null) { - state = State.READ_COMMON_HEADER; - out.add(frame); - } - return; - } catch (IllegalArgumentException e) { - state = State.FRAME_ERROR; - fireInvalidFrameException(ctx); - } - return; - - case READ_SETTINGS_FRAME: - if (spdySettingsFrame == null) { - // Validate frame length against number of entries - if (buffer.readableBytes() < 4) { - return; - } - int numEntries = getUnsignedInt(buffer, buffer.readerIndex()); - buffer.skipBytes(4); - length -= 4; - - // Each ID/Value entry is 8 bytes - if ((length & 0x07) != 0 || length >> 3 != numEntries) { - state = State.FRAME_ERROR; - fireInvalidFrameException(ctx); - return; - } - - spdySettingsFrame = new DefaultSpdySettingsFrame(); - - boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; - spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); - } - - int readableEntries = Math.min(buffer.readableBytes() >> 3, length >> 3); - for (int i = 0; i < readableEntries; i ++) { - byte ID_flags = buffer.getByte(buffer.readerIndex()); - int ID = getUnsignedMedium(buffer, buffer.readerIndex() + 1); - int value = getSignedInt(buffer, buffer.readerIndex() + 4); - buffer.skipBytes(8); - - if (!spdySettingsFrame.isSet(ID)) { - boolean persistVal = (ID_flags & SPDY_SETTINGS_PERSIST_VALUE) != 0; - boolean persisted = (ID_flags & SPDY_SETTINGS_PERSISTED) != 0; - spdySettingsFrame.setValue(ID, value, persistVal, persisted); - } - } - - length -= 8 * readableEntries; - if (length == 0) { - state = State.READ_COMMON_HEADER; - Object frame = spdySettingsFrame; - spdySettingsFrame = null; - out.add(frame); - return; - } - return; - - case READ_HEADER_BLOCK_FRAME: - try { - spdyHeadersFrame = readHeaderBlockFrame(buffer); - if (spdyHeadersFrame != null) { - if (length == 0) { - state = State.READ_COMMON_HEADER; - Object frame = spdyHeadersFrame; - spdyHeadersFrame = null; - out.add(frame); + while (true) { + switch(state) { + case READ_COMMON_HEADER: + if (buffer.readableBytes() < SPDY_HEADER_SIZE) { return; } - state = State.READ_HEADER_BLOCK; - } - return; - } catch (IllegalArgumentException e) { - state = State.FRAME_ERROR; - fireInvalidFrameException(ctx); - return; - } - case READ_HEADER_BLOCK: - int compressedBytes = Math.min(buffer.readableBytes(), length); - ByteBuf compressed = buffer.slice(buffer.readerIndex(), compressedBytes); + int frameOffset = buffer.readerIndex(); + int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; + int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; + buffer.skipBytes(SPDY_HEADER_SIZE); - try { - headerBlockDecoder.decode(compressed, spdyHeadersFrame); - } catch (Exception e) { - state = State.FRAME_ERROR; - spdyHeadersFrame = null; - ctx.fireExceptionCaught(e); - return; - } + boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; - int readBytes = compressedBytes - compressed.readableBytes(); - buffer.skipBytes(readBytes); - length -= readBytes; + int version; + int type; + if (control) { + // Decode control frame common header + version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; + type = getUnsignedShort(buffer, frameOffset + SPDY_HEADER_TYPE_OFFSET); + streamId = 0; // Default to session Stream-ID + } else { + // Decode data frame common header + version = spdyVersion; // Default to expected version + type = SPDY_DATA_FRAME; + streamId = getUnsignedInt(buffer, frameOffset); + } - if (spdyHeadersFrame != null && - (spdyHeadersFrame.isInvalid() || spdyHeadersFrame.isTruncated())) { + flags = buffer.getByte(flagsOffset); + length = getUnsignedMedium(buffer, lengthOffset); + + // Check version first then validity + if (version != spdyVersion) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid SPDY Version"); + } else if (!isValidFrameHeader(streamId, type, flags, length)) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid Frame Error"); + } else { + state = getNextState(type, length); + } + break; + + case READ_DATA_FRAME: + if (length == 0) { + state = State.READ_COMMON_HEADER; + delegate.readDataFrame(streamId, hasFlag(flags, SPDY_DATA_FLAG_FIN), Unpooled.buffer(0)); + break; + } + + // Generate data frames that do not exceed maxChunkSize + int dataLength = Math.min(maxChunkSize, length); + + // Wait until entire frame is readable + if (buffer.readableBytes() < dataLength) { + return; + } + + ByteBuf data = buffer.alloc().buffer(dataLength); + data.writeBytes(buffer, dataLength); + length -= dataLength; + + if (length == 0) { + state = State.READ_COMMON_HEADER; + } + + last = length == 0 && hasFlag(flags, SPDY_DATA_FLAG_FIN); + + delegate.readDataFrame(streamId, last, data); + break; + + case READ_SYN_STREAM_FRAME: + if (buffer.readableBytes() < 10) { + return; + } + + int offset = buffer.readerIndex(); + streamId = getUnsignedInt(buffer, offset); + int associatedToStreamId = getUnsignedInt(buffer, offset + 4); + byte priority = (byte) (buffer.getByte(offset + 8) >> 5 & 0x07); + last = hasFlag(flags, SPDY_FLAG_FIN); + boolean unidirectional = hasFlag(flags, SPDY_FLAG_UNIDIRECTIONAL); + buffer.skipBytes(10); + length -= 10; + + if (streamId == 0) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid SYN_STREAM Frame"); + } else { + state = State.READ_HEADER_BLOCK; + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, last, unidirectional); + } + break; + + case READ_SYN_REPLY_FRAME: + if (buffer.readableBytes() < 4) { + return; + } + + streamId = getUnsignedInt(buffer, buffer.readerIndex()); + last = hasFlag(flags, SPDY_FLAG_FIN); + + buffer.skipBytes(4); + length -= 4; + + if (streamId == 0) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid SYN_REPLY Frame"); + } else { + state = State.READ_HEADER_BLOCK; + delegate.readSynReplyFrame(streamId, last); + } + break; + + case READ_RST_STREAM_FRAME: + if (buffer.readableBytes() < 8) { + return; + } + + streamId = getUnsignedInt(buffer, buffer.readerIndex()); + statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + if (streamId == 0 || statusCode == 0) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid RST_STREAM Frame"); + } else { + state = State.READ_COMMON_HEADER; + delegate.readRstStreamFrame(streamId, statusCode); + } + break; + + case READ_SETTINGS_FRAME: + if (buffer.readableBytes() < 4) { + return; + } + + boolean clear = hasFlag(flags, SPDY_SETTINGS_CLEAR); + + numSettings = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // Validate frame length against number of entries. Each ID/Value entry is 8 bytes. + if ((length & 0x07) != 0 || length >> 3 != numSettings) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid SETTINGS Frame"); + } else { + state = State.READ_SETTING; + delegate.readSettingsFrame(clear); + } + break; + + case READ_SETTING: + if (numSettings == 0) { + state = State.READ_COMMON_HEADER; + delegate.readSettingsEnd(); + break; + } + + if (buffer.readableBytes() < 8) { + return; + } + + byte settingsFlags = buffer.getByte(buffer.readerIndex()); + int id = getUnsignedMedium(buffer, buffer.readerIndex() + 1); + int value = getSignedInt(buffer, buffer.readerIndex() + 4); + boolean persistValue = hasFlag(settingsFlags, SPDY_SETTINGS_PERSIST_VALUE); + boolean persisted = hasFlag(settingsFlags, SPDY_SETTINGS_PERSISTED); + buffer.skipBytes(8); + + --numSettings; + + delegate.readSetting(id, value, persistValue, persisted); + break; + + case READ_PING_FRAME: + if (buffer.readableBytes() < 4) { + return; + } + + int pingId = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); - Object frame = spdyHeadersFrame; - spdyHeadersFrame = null; - if (length == 0) { - headerBlockDecoder.reset(); state = State.READ_COMMON_HEADER; - } - out.add(frame); - return; + delegate.readPingFrame(pingId); + break; + + case READ_GOAWAY_FRAME: + if (buffer.readableBytes() < 8) { + return; + } + + int lastGoodStreamId = getUnsignedInt(buffer, buffer.readerIndex()); + statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + state = State.READ_COMMON_HEADER; + delegate.readGoAwayFrame(lastGoodStreamId, statusCode); + break; + + case READ_HEADERS_FRAME: + if (buffer.readableBytes() < 4) { + return; + } + + streamId = getUnsignedInt(buffer, buffer.readerIndex()); + last = hasFlag(flags, SPDY_FLAG_FIN); + + buffer.skipBytes(4); + length -= 4; + + if (streamId == 0) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid HEADERS Frame"); + } else { + state = State.READ_HEADER_BLOCK; + delegate.readHeadersFrame(streamId, last); + } + break; + + case READ_WINDOW_UPDATE_FRAME: + if (buffer.readableBytes() < 8) { + return; + } + + streamId = getUnsignedInt(buffer, buffer.readerIndex()); + int deltaWindowSize = getUnsignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + if (deltaWindowSize == 0) { + state = State.FRAME_ERROR; + delegate.readFrameError("Invalid WINDOW_UPDATE Frame"); + } else { + state = State.READ_COMMON_HEADER; + delegate.readWindowUpdateFrame(streamId, deltaWindowSize); + } + break; + + case READ_HEADER_BLOCK: + if (length == 0) { + state = State.READ_COMMON_HEADER; + delegate.readHeaderBlockEnd(); + break; + } + + if (!buffer.isReadable()) { + return; + } + + int compressedBytes = Math.min(buffer.readableBytes(), length); + ByteBuf headerBlock = buffer.alloc().buffer(compressedBytes); + headerBlock.writeBytes(buffer, compressedBytes); + length -= compressedBytes; + + delegate.readHeaderBlock(headerBlock); + break; + + case DISCARD_FRAME: + int numBytes = Math.min(buffer.readableBytes(), length); + buffer.skipBytes(numBytes); + length -= numBytes; + if (length == 0) { + state = State.READ_COMMON_HEADER; + break; + } + return; + + case FRAME_ERROR: + buffer.skipBytes(buffer.readableBytes()); + return; + + default: + throw new Error("Shouldn't reach here."); } - - if (length == 0) { - Object frame = spdyHeadersFrame; - spdyHeadersFrame = null; - headerBlockDecoder.reset(); - state = State.READ_COMMON_HEADER; - if (frame != null) { - out.add(frame); - } - } - return; - - case READ_DATA_FRAME: - if (streamId == 0) { - state = State.FRAME_ERROR; - fireProtocolException(ctx, "Received invalid data frame"); - return; - } - - // Generate data frames that do not exceed maxChunkSize - int dataLength = Math.min(maxChunkSize, length); - - // Wait until entire frame is readable - if (buffer.readableBytes() < dataLength) { - return; - } - - ByteBuf data = ctx.alloc().buffer(dataLength); - data.writeBytes(buffer, dataLength); - SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamId, data); - length -= dataLength; - - if (length == 0) { - spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); - state = State.READ_COMMON_HEADER; - } - out.add(spdyDataFrame); - return; - - case DISCARD_FRAME: - int numBytes = Math.min(buffer.readableBytes(), length); - buffer.skipBytes(numBytes); - length -= numBytes; - if (length == 0) { - state = State.READ_COMMON_HEADER; - } - return; - - case FRAME_ERROR: - buffer.skipBytes(buffer.readableBytes()); - return; - - default: - throw new Error("Shouldn't reach here."); } } - private State readCommonHeader(ByteBuf buffer) { - // Wait until entire header is readable - if (buffer.readableBytes() < SPDY_HEADER_SIZE) { - return State.READ_COMMON_HEADER; - } + private static boolean hasFlag(byte flags, byte flag) { + return (flags & flag) != 0; + } - int frameOffset = buffer.readerIndex(); - int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; - int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; - buffer.skipBytes(SPDY_HEADER_SIZE); - - // Read common header fields - boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; - flags = buffer.getByte(flagsOffset); - length = getUnsignedMedium(buffer, lengthOffset); - - if (control) { - // Decode control frame common header - version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; - - int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; - type = getUnsignedShort(buffer, typeOffset); - - streamId = 0; - } else { - // Decode data frame common header - version = spdyVersion; // Default to expected version - - type = SPDY_DATA_FRAME; - - streamId = getUnsignedInt(buffer, frameOffset); - } - // Check version first then validity - if (version != spdyVersion || !isValidFrameHeader()) { - return State.FRAME_ERROR; - } - - // Make sure decoder will produce a frame or consume input - State nextState; - if (willGenerateFrame()) { - switch (type) { + private static State getNextState(int type, int length) { + switch (type) { case SPDY_DATA_FRAME: - nextState = State.READ_DATA_FRAME; - break; + return State.READ_DATA_FRAME; case SPDY_SYN_STREAM_FRAME: + return State.READ_SYN_STREAM_FRAME; + case SPDY_SYN_REPLY_FRAME: - case SPDY_HEADERS_FRAME: - nextState = State.READ_HEADER_BLOCK_FRAME; - break; + return State.READ_SYN_REPLY_FRAME; + + case SPDY_RST_STREAM_FRAME: + return State.READ_RST_STREAM_FRAME; case SPDY_SETTINGS_FRAME: - nextState = State.READ_SETTINGS_FRAME; - break; + return State.READ_SETTINGS_FRAME; + + case SPDY_PING_FRAME: + return State.READ_PING_FRAME; + + case SPDY_GOAWAY_FRAME: + return State.READ_GOAWAY_FRAME; + + case SPDY_HEADERS_FRAME: + return State.READ_HEADERS_FRAME; + + case SPDY_WINDOW_UPDATE_FRAME: + return State.READ_WINDOW_UPDATE_FRAME; default: - nextState = State.READ_CONTROL_FRAME; - } - } else if (length != 0) { - nextState = State.DISCARD_FRAME; - } else { - nextState = State.READ_COMMON_HEADER; + if (length != 0) { + return State.DISCARD_FRAME; + } else { + return State.READ_COMMON_HEADER; + } } - return nextState; } - private Object readControlFrame(ByteBuf buffer) { - int streamId; - int statusCode; + private static boolean isValidFrameHeader(int streamId, int type, byte flags, int length) { switch (type) { - case SPDY_RST_STREAM_FRAME: - if (buffer.readableBytes() < 8) { - return null; - } + case SPDY_DATA_FRAME: + return streamId != 0; - streamId = getUnsignedInt(buffer, buffer.readerIndex()); - statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); - buffer.skipBytes(8); + case SPDY_SYN_STREAM_FRAME: + return length >= 10; - return new DefaultSpdyRstStreamFrame(streamId, statusCode); + case SPDY_SYN_REPLY_FRAME: + return length >= 4; - case SPDY_PING_FRAME: - if (buffer.readableBytes() < 4) { - return null; - } + case SPDY_RST_STREAM_FRAME: + return flags == 0 && length == 8; - int ID = getSignedInt(buffer, buffer.readerIndex()); - buffer.skipBytes(4); + case SPDY_SETTINGS_FRAME: + return length >= 4; - return new DefaultSpdyPingFrame(ID); + case SPDY_PING_FRAME: + return length == 4; - case SPDY_GOAWAY_FRAME: - if (buffer.readableBytes() < 8) { - return null; - } + case SPDY_GOAWAY_FRAME: + return length == 8; - int lastGoodStreamId = getUnsignedInt(buffer, buffer.readerIndex()); - statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); - buffer.skipBytes(8); + case SPDY_HEADERS_FRAME: + return length >= 4; - return new DefaultSpdyGoAwayFrame(lastGoodStreamId, statusCode); + case SPDY_WINDOW_UPDATE_FRAME: + return length == 8; - case SPDY_WINDOW_UPDATE_FRAME: - if (buffer.readableBytes() < 8) { - return null; - } - - streamId = getUnsignedInt(buffer, buffer.readerIndex()); - int deltaWindowSize = getUnsignedInt(buffer, buffer.readerIndex() + 4); - buffer.skipBytes(8); - - return new DefaultSpdyWindowUpdateFrame(streamId, deltaWindowSize); - - default: - throw new Error("Shouldn't reach here."); + default: + return true; } } - - private SpdyHeadersFrame readHeaderBlockFrame(ByteBuf buffer) { - int streamId; - switch (type) { - case SPDY_SYN_STREAM_FRAME: - if (buffer.readableBytes() < 10) { - return null; - } - - int offset = buffer.readerIndex(); - streamId = getUnsignedInt(buffer, offset); - int associatedToStreamId = getUnsignedInt(buffer, offset + 4); - byte priority = (byte) (buffer.getByte(offset + 8) >> 5 & 0x07); - buffer.skipBytes(10); - length -= 10; - - SpdySynStreamFrame spdySynStreamFrame = - new DefaultSpdySynStreamFrame(streamId, associatedToStreamId, priority); - spdySynStreamFrame.setLast((flags & SPDY_FLAG_FIN) != 0); - spdySynStreamFrame.setUnidirectional((flags & SPDY_FLAG_UNIDIRECTIONAL) != 0); - - return spdySynStreamFrame; - - case SPDY_SYN_REPLY_FRAME: - if (buffer.readableBytes() < 4) { - return null; - } - - streamId = getUnsignedInt(buffer, buffer.readerIndex()); - buffer.skipBytes(4); - length -= 4; - - SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); - spdySynReplyFrame.setLast((flags & SPDY_FLAG_FIN) != 0); - - return spdySynReplyFrame; - - case SPDY_HEADERS_FRAME: - if (buffer.readableBytes() < 4) { - return null; - } - - streamId = getUnsignedInt(buffer, buffer.readerIndex()); - buffer.skipBytes(4); - length -= 4; - - SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamId); - spdyHeadersFrame.setLast((flags & SPDY_FLAG_FIN) != 0); - - return spdyHeadersFrame; - - default: - throw new Error("Shouldn't reach here."); - } - } - - private boolean isValidFrameHeader() { - switch (type) { - case SPDY_DATA_FRAME: - return streamId != 0; - - case SPDY_SYN_STREAM_FRAME: - return length >= 10; - - case SPDY_SYN_REPLY_FRAME: - return length >= 4; - - case SPDY_RST_STREAM_FRAME: - return flags == 0 && length == 8; - - case SPDY_SETTINGS_FRAME: - return length >= 4; - - case SPDY_PING_FRAME: - return length == 4; - - case SPDY_GOAWAY_FRAME: - return length == 8; - - case SPDY_HEADERS_FRAME: - return length >= 4; - - case SPDY_WINDOW_UPDATE_FRAME: - return length == 8; - - default: - return true; - } - } - - private boolean willGenerateFrame() { - switch (type) { - case SPDY_DATA_FRAME: - case SPDY_SYN_STREAM_FRAME: - case SPDY_SYN_REPLY_FRAME: - case SPDY_RST_STREAM_FRAME: - case SPDY_SETTINGS_FRAME: - case SPDY_PING_FRAME: - case SPDY_GOAWAY_FRAME: - case SPDY_HEADERS_FRAME: - case SPDY_WINDOW_UPDATE_FRAME: - return true; - - default: - return false; - } - } - - private static void fireInvalidFrameException(ChannelHandlerContext ctx) { - ctx.fireExceptionCaught(INVALID_FRAME); - } - - private static void fireProtocolException(ChannelHandlerContext ctx, String message) { - ctx.fireExceptionCaught(new SpdyProtocolException(message)); - } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoderDelegate.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoderDelegate.java new file mode 100644 index 0000000000..52815c787c --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoderDelegate.java @@ -0,0 +1,99 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ByteBuf; + +/** + * Callback interface for {@link SpdyFrameDecoder}. + */ +public interface SpdyFrameDecoderDelegate { + + /** + * Called when a DATA frame is received. + */ + void readDataFrame(int streamId, boolean last, ByteBuf data); + + /** + * Called when a SYN_STREAM frame is received. + * The Name/Value Header Block is not included. See readHeaderBlock(). + */ + void readSynStreamFrame( + int streamId, int associatedToStreamId, byte priority, boolean last, boolean unidirectional); + + /** + * Called when a SYN_REPLY frame is received. + * The Name/Value Header Block is not included. See readHeaderBlock(). + */ + void readSynReplyFrame(int streamId, boolean last); + + /** + * Called when a RST_STREAM frame is received. + */ + void readRstStreamFrame(int streamId, int statusCode); + + /** + * Called when a SETTINGS frame is received. + * Settings are not included. See readSetting(). + */ + void readSettingsFrame(boolean clearPersisted); + + /** + * Called when an individual setting within a SETTINGS frame is received. + */ + void readSetting(int id, int value, boolean persistValue, boolean persisted); + + /** + * Called when the entire SETTINGS frame has been received. + */ + void readSettingsEnd(); + + /** + * Called when a PING frame is received. + */ + void readPingFrame(int id); + + /** + * Called when a GOAWAY frame is received. + */ + void readGoAwayFrame(int lastGoodStreamId, int statusCode); + + /** + * Called when a HEADERS frame is received. + * The Name/Value Header Block is not included. See readHeaderBlock(). + */ + void readHeadersFrame(int streamId, boolean last); + + /** + * Called when a WINDOW_UPDATE frame is received. + */ + void readWindowUpdateFrame(int streamId, int deltaWindowSize); + + /** + * Called when the header block within a SYN_STREAM, SYN_REPLY, or HEADERS frame is received. + */ + void readHeaderBlock(ByteBuf headerBlock); + + /** + * Called when an entire header block has been received. + */ + void readHeaderBlockEnd(); + + /** + * Called when an unrecoverable session error has occurred. + */ + void readFrameError(String 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 762c12ad3e..636480150a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -16,12 +16,9 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToByteEncoder; -import io.netty.handler.codec.UnsupportedMessageTypeException; +import io.netty.buffer.ByteBufAllocator; +import java.nio.ByteOrder; import java.util.Set; import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; @@ -29,188 +26,138 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; /** * Encodes a SPDY Frame into a {@link ByteBuf}. */ -public class SpdyFrameEncoder extends MessageToByteEncoder { +public class SpdyFrameEncoder { private final int version; - private final SpdyHeaderBlockEncoder headerBlockEncoder; /** - * Creates a new instance with the specified {@code version} and the - * default {@code compressionLevel (6)}, {@code windowBits (15)}, - * and {@code memLevel (8)}. + * Creates a new instance with the specified {@code spdyVersion}. */ - public SpdyFrameEncoder(SpdyVersion version) { - this(version, 6, 15, 8); - } - - /** - * Creates a new instance with the specified parameters. - */ - public SpdyFrameEncoder(SpdyVersion version, int compressionLevel, int windowBits, int memLevel) { - this(version, SpdyHeaderBlockEncoder.newInstance( - version, compressionLevel, windowBits, memLevel)); - } - - protected SpdyFrameEncoder(SpdyVersion version, SpdyHeaderBlockEncoder headerBlockEncoder) { - if (version == null) { - throw new NullPointerException("version"); + public SpdyFrameEncoder(SpdyVersion spdyVersion) { + if (spdyVersion == null) { + throw new NullPointerException("spdyVersion"); } - this.version = version.getVersion(); - this.headerBlockEncoder = headerBlockEncoder; + version = spdyVersion.getVersion(); } - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - ctx.channel().closeFuture().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - headerBlockEncoder.end(); - } - }); + private void writeControlFrameHeader(ByteBuf buffer, int type, byte flags, int length) { + buffer.writeShort(version | 0x8000); + buffer.writeShort(type); + buffer.writeByte(flags); + buffer.writeMedium(length); } - @Override - protected void encode(ChannelHandlerContext ctx, SpdyFrame msg, ByteBuf out) throws Exception { - if (msg instanceof SpdyDataFrame) { + public ByteBuf encodeDataFrame(ByteBufAllocator allocator, int streamId, boolean last, ByteBuf data) { + byte flags = last ? SPDY_DATA_FLAG_FIN : 0; + int length = data.readableBytes(); + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + frame.writeInt(streamId & 0x7FFFFFFF); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeBytes(data, data.readerIndex(), length); + return frame; + } - SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; - ByteBuf data = spdyDataFrame.content(); - byte flags = spdyDataFrame.isLast() ? SPDY_DATA_FLAG_FIN : 0; - out.ensureWritable(SPDY_HEADER_SIZE + data.readableBytes()); - out.writeInt(spdyDataFrame.getStreamId() & 0x7FFFFFFF); - out.writeByte(flags); - out.writeMedium(data.readableBytes()); - out.writeBytes(data, data.readerIndex(), data.readableBytes()); + public ByteBuf encodeSynStreamFrame(ByteBufAllocator allocator, int streamId, int associatedToStreamId, + byte priority, boolean last, boolean unidirectional, ByteBuf headerBlock) { + int headerBlockLength = headerBlock.readableBytes(); + byte flags = last ? SPDY_FLAG_FIN : 0; + if (unidirectional) { + flags |= SPDY_FLAG_UNIDIRECTIONAL; + } + int length = 10 + headerBlockLength; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_SYN_STREAM_FRAME, flags, length); + frame.writeInt(streamId); + frame.writeInt(associatedToStreamId); + frame.writeShort((priority & 0xFF) << 13); + frame.writeBytes(headerBlock, headerBlock.readerIndex(), headerBlockLength); + return frame; + } - } else if (msg instanceof SpdySynStreamFrame) { + public ByteBuf encodeSynReplyFrame(ByteBufAllocator allocator, int streamId, boolean last, ByteBuf headerBlock) { + int headerBlockLength = headerBlock.readableBytes(); + byte flags = last ? SPDY_FLAG_FIN : 0; + int length = 4 + headerBlockLength; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_SYN_REPLY_FRAME, flags, length); + frame.writeInt(streamId); + frame.writeBytes(headerBlock, headerBlock.readerIndex(), headerBlockLength); + return frame; + } - SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; - ByteBuf data = headerBlockEncoder.encode(spdySynStreamFrame); - try { - byte flags = spdySynStreamFrame.isLast() ? SPDY_FLAG_FIN : 0; - if (spdySynStreamFrame.isUnidirectional()) { - flags |= SPDY_FLAG_UNIDIRECTIONAL; - } - int headerBlockLength = data.readableBytes(); - int length = 10 + headerBlockLength; - out.ensureWritable(SPDY_HEADER_SIZE + length); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_SYN_STREAM_FRAME); - out.writeByte(flags); - out.writeMedium(length); - out.writeInt(spdySynStreamFrame.getStreamId()); - out.writeInt(spdySynStreamFrame.getAssociatedToStreamId()); - out.writeShort((spdySynStreamFrame.getPriority() & 0xFF) << 13); - out.writeBytes(data, data.readerIndex(), headerBlockLength); - } finally { - data.release(); - } + public ByteBuf encodeRstStreamFrame(ByteBufAllocator allocator, int streamId, int statusCode) { + byte flags = 0; + int length = 8; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_RST_STREAM_FRAME, flags, length); + frame.writeInt(streamId); + frame.writeInt(statusCode); + return frame; + } - } else if (msg instanceof SpdySynReplyFrame) { + public ByteBuf encodeSettingsFrame(ByteBufAllocator allocator, SpdySettingsFrame spdySettingsFrame) { + Set ids = spdySettingsFrame.getIds(); + int numSettings = ids.size(); - SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; - ByteBuf data = headerBlockEncoder.encode(spdySynReplyFrame); - try { - byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; - int headerBlockLength = data.readableBytes(); - int length = 4 + headerBlockLength; - out.ensureWritable(SPDY_HEADER_SIZE + length); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_SYN_REPLY_FRAME); - out.writeByte(flags); - out.writeMedium(length); - out.writeInt(spdySynReplyFrame.getStreamId()); - out.writeBytes(data, data.readerIndex(), headerBlockLength); - } finally { - data.release(); - } - - } else if (msg instanceof SpdyRstStreamFrame) { - - SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; - out.ensureWritable(SPDY_HEADER_SIZE + 8); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_RST_STREAM_FRAME); - out.writeInt(8); - out.writeInt(spdyRstStreamFrame.getStreamId()); - out.writeInt(spdyRstStreamFrame.getStatus().getCode()); - - } else if (msg instanceof SpdySettingsFrame) { - - SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; - byte flags = spdySettingsFrame.clearPreviouslyPersistedSettings() ? + byte flags = spdySettingsFrame.clearPreviouslyPersistedSettings() ? SPDY_SETTINGS_CLEAR : 0; - Set IDs = spdySettingsFrame.getIds(); - int numEntries = IDs.size(); - int length = 4 + numEntries * 8; - out.ensureWritable(SPDY_HEADER_SIZE + length); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_SETTINGS_FRAME); - out.writeByte(flags); - out.writeMedium(length); - out.writeInt(numEntries); - for (Integer id: IDs) { - byte ID_flags = 0; - if (spdySettingsFrame.isPersistValue(id)) { - ID_flags |= SPDY_SETTINGS_PERSIST_VALUE; - } - if (spdySettingsFrame.isPersisted(id)) { - ID_flags |= SPDY_SETTINGS_PERSISTED; - } - out.writeByte(ID_flags); - out.writeMedium(id); - out.writeInt(spdySettingsFrame.getValue(id)); + int length = 4 + 8 * numSettings; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_SETTINGS_FRAME, flags, length); + frame.writeInt(numSettings); + for (Integer id : ids) { + flags = 0; + if (spdySettingsFrame.isPersistValue(id)) { + flags |= SPDY_SETTINGS_PERSIST_VALUE; } - - } else if (msg instanceof SpdyPingFrame) { - - SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; - out.ensureWritable(SPDY_HEADER_SIZE + 4); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_PING_FRAME); - out.writeInt(4); - out.writeInt(spdyPingFrame.getId()); - - } else if (msg instanceof SpdyGoAwayFrame) { - - SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; - out.ensureWritable(SPDY_HEADER_SIZE + 8); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_GOAWAY_FRAME); - out.writeInt(8); - out.writeInt(spdyGoAwayFrame.getLastGoodStreamId()); - out.writeInt(spdyGoAwayFrame.getStatus().getCode()); - - } else if (msg instanceof SpdyHeadersFrame) { - - SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; - ByteBuf data = headerBlockEncoder.encode(spdyHeadersFrame); - try { - byte flags = spdyHeadersFrame.isLast() ? SPDY_FLAG_FIN : 0; - int headerBlockLength = data.readableBytes(); - int length = 4 + headerBlockLength; - out.ensureWritable(SPDY_HEADER_SIZE + length); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_HEADERS_FRAME); - out.writeByte(flags); - out.writeMedium(length); - out.writeInt(spdyHeadersFrame.getStreamId()); - out.writeBytes(data, data.readerIndex(), headerBlockLength); - } finally { - data.release(); + if (spdySettingsFrame.isPersisted(id)) { + flags |= SPDY_SETTINGS_PERSISTED; } - - } else if (msg instanceof SpdyWindowUpdateFrame) { - - SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; - out.ensureWritable(SPDY_HEADER_SIZE + 8); - out.writeShort(version | 0x8000); - out.writeShort(SPDY_WINDOW_UPDATE_FRAME); - out.writeInt(8); - out.writeInt(spdyWindowUpdateFrame.getStreamId()); - out.writeInt(spdyWindowUpdateFrame.getDeltaWindowSize()); - } else { - throw new UnsupportedMessageTypeException(msg); + frame.writeByte(flags); + frame.writeMedium(id); + frame.writeInt(spdySettingsFrame.getValue(id)); } + return frame; + } + + public ByteBuf encodePingFrame(ByteBufAllocator allocator, int id) { + byte flags = 0; + int length = 4; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_PING_FRAME, flags, length); + frame.writeInt(id); + return frame; + } + + public ByteBuf encodeGoAwayFrame(ByteBufAllocator allocator, int lastGoodStreamId, int statusCode) { + byte flags = 0; + int length = 8; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_GOAWAY_FRAME, flags, length); + frame.writeInt(lastGoodStreamId); + frame.writeInt(statusCode); + return frame; + } + + public ByteBuf encodeHeadersFrame(ByteBufAllocator allocator, int streamId, boolean last, ByteBuf headerBlock) { + int headerBlockLength = headerBlock.readableBytes(); + byte flags = last ? SPDY_FLAG_FIN : 0; + int length = 4 + headerBlockLength; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_HEADERS_FRAME, flags, length); + frame.writeInt(streamId); + frame.writeBytes(headerBlock, headerBlock.readerIndex(), headerBlockLength); + return frame; + } + + public ByteBuf encodeWindowUpdateFrame(ByteBufAllocator allocator, int streamId, int deltaWindowSize) { + byte flags = 0; + int length = 8; + ByteBuf frame = allocator.ioBuffer(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN); + writeControlFrameHeader(frame, SPDY_WINDOW_UPDATE_FRAME, flags, length); + frame.writeInt(streamId); + frame.writeInt(deltaWindowSize); + return frame; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecoder.java index 731e7f2b6d..a4b80aac16 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -19,11 +19,24 @@ import io.netty.buffer.ByteBuf; abstract class SpdyHeaderBlockDecoder { - static SpdyHeaderBlockDecoder newInstance(SpdyVersion version, int maxHeaderSize) { - return new SpdyHeaderBlockZlibDecoder(version, maxHeaderSize); + static SpdyHeaderBlockDecoder newInstance(SpdyVersion spdyVersion, int maxHeaderSize) { + return new SpdyHeaderBlockZlibDecoder(spdyVersion, maxHeaderSize); } - abstract void decode(ByteBuf encoded, SpdyHeadersFrame frame) throws Exception; - abstract void reset(); + /** + * Decodes a SPDY Header Block, adding the Name/Value pairs to the given Headers frame. + * If the header block is malformed, the Headers frame will be marked as invalid. + * A stream error with status code PROTOCOL_ERROR must be issued in response to an invalid frame. + * + * @param headerBlock the HeaderBlock to decode + * @param frame the Headers frame that receives the Name/Value pairs + * @throws Exception If the header block is malformed in a way that prevents any future + * decoding of any other header blocks, an exception will be thrown. + * A session error with status code PROTOCOL_ERROR must be issued. + */ + abstract void decode(ByteBuf headerBlock, SpdyHeadersFrame frame) throws Exception; + + abstract void endHeaderBlock(SpdyHeadersFrame frame) throws Exception; + abstract void end(); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java index 0d2a00c659..a91f0b7784 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -23,20 +23,33 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { private static final int LENGTH_FIELD_SIZE = 4; - private final int version; private final int maxHeaderSize; - // Header block decoding fields + private State state; + private int headerSize; - private int numHeaders = -1; + private int numHeaders; + private int length; + private String name; + + private enum State { + READ_NUM_HEADERS, + READ_NAME_LENGTH, + READ_NAME, + SKIP_NAME, + READ_VALUE_LENGTH, + READ_VALUE, + SKIP_VALUE, + END_HEADER_BLOCK, + ERROR + } public SpdyHeaderBlockRawDecoder(SpdyVersion spdyVersion, int maxHeaderSize) { if (spdyVersion == null) { throw new NullPointerException("spdyVersion"); } - - version = spdyVersion.getVersion(); this.maxHeaderSize = maxHeaderSize; + this.state = State.READ_NUM_HEADERS; } private static int readLengthField(ByteBuf buffer) { @@ -46,133 +59,218 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { } @Override - void decode(ByteBuf encoded, SpdyHeadersFrame frame) throws Exception { - if (encoded == null) { - throw new NullPointerException("encoded"); + void decode(ByteBuf headerBlock, SpdyHeadersFrame frame) throws Exception { + if (headerBlock == null) { + throw new NullPointerException("headerBlock"); } if (frame == null) { throw new NullPointerException("frame"); } - if (numHeaders == -1) { - // Read number of Name/Value pairs - if (encoded.readableBytes() < LENGTH_FIELD_SIZE) { - return; - } - numHeaders = readLengthField(encoded); - if (numHeaders < 0) { - frame.setInvalid(); - return; - } - } + int skipLength; + while (headerBlock.isReadable()) { + switch(state) { + case READ_NUM_HEADERS: + if (headerBlock.readableBytes() < LENGTH_FIELD_SIZE) { + return; + } - while (numHeaders > 0) { - int headerSize = this.headerSize; - encoded.markReaderIndex(); + numHeaders = readLengthField(headerBlock); - // Try to read length of name - if (encoded.readableBytes() < LENGTH_FIELD_SIZE) { - encoded.resetReaderIndex(); - return; - } - int nameLength = readLengthField(encoded); + if (numHeaders < 0) { + state = State.ERROR; + frame.setInvalid(); + } else if (numHeaders == 0) { + state = State.END_HEADER_BLOCK; + } else { + state = State.READ_NAME_LENGTH; + } + break; - // Recipients of a zero-length name must issue a stream error - if (nameLength <= 0) { - frame.setInvalid(); - return; - } - headerSize += nameLength; - if (headerSize > maxHeaderSize) { - frame.setTruncated(); - return; - } + case READ_NAME_LENGTH: + if (headerBlock.readableBytes() < LENGTH_FIELD_SIZE) { + return; + } - // Try to read name - if (encoded.readableBytes() < nameLength) { - encoded.resetReaderIndex(); - return; - } - byte[] nameBytes = new byte[nameLength]; - encoded.readBytes(nameBytes); - String name = new String(nameBytes, "UTF-8"); + length = readLengthField(headerBlock); - // Check for identically named headers - if (frame.headers().contains(name)) { - frame.setInvalid(); - return; - } + // Recipients of a zero-length name must issue a stream error + if (length <= 0) { + state = State.ERROR; + frame.setInvalid(); + } else if (length > maxHeaderSize || headerSize > maxHeaderSize - length) { + headerSize = maxHeaderSize + 1; + state = State.SKIP_NAME; + frame.setTruncated(); + } else { + headerSize += length; + state = State.READ_NAME; + } + break; - // Try to read length of value - if (encoded.readableBytes() < LENGTH_FIELD_SIZE) { - encoded.resetReaderIndex(); - return; - } - int valueLength = readLengthField(encoded); + case READ_NAME: + if (headerBlock.readableBytes() < length) { + return; + } - // Recipients of illegal value fields must issue a stream error - if (valueLength < 0) { - frame.setInvalid(); - return; - } + byte[] nameBytes = new byte[length]; + headerBlock.readBytes(nameBytes); + name = new String(nameBytes, "UTF-8"); - // SPDY/3 allows zero-length (empty) header values - if (valueLength == 0) { - frame.headers().add(name, ""); - numHeaders --; - this.headerSize = headerSize; - continue; - } + // Check for identically named headers + if (frame.headers().contains(name)) { + state = State.ERROR; + frame.setInvalid(); + } else { + state = State.READ_VALUE_LENGTH; + } + break; - headerSize += valueLength; - if (headerSize > maxHeaderSize) { - frame.setTruncated(); - return; - } + case SKIP_NAME: + skipLength = Math.min(headerBlock.readableBytes(), length); + headerBlock.skipBytes(skipLength); + length -= skipLength; - // Try to read value - if (encoded.readableBytes() < valueLength) { - encoded.resetReaderIndex(); - return; - } - byte[] valueBytes = new byte[valueLength]; - encoded.readBytes(valueBytes); + if (length == 0) { + state = State.READ_VALUE_LENGTH; + } + break; + + case READ_VALUE_LENGTH: + if (headerBlock.readableBytes() < LENGTH_FIELD_SIZE) { + return; + } + + length = readLengthField(headerBlock); - // Add Name/Value pair to headers - int index = 0; - int offset = 0; - while (index < valueLength) { - while (index < valueBytes.length && valueBytes[index] != (byte) 0) { - index ++; - } - if (index < valueBytes.length && valueBytes[index + 1] == (byte) 0) { - // Received multiple, in-sequence NULL characters // Recipients of illegal value fields must issue a stream error - frame.setInvalid(); - return; - } - String value = new String(valueBytes, offset, index - offset, "UTF-8"); + if (length < 0) { + state = State.ERROR; + frame.setInvalid(); + } else if (length == 0) { + if (!frame.isTruncated()) { + // SPDY/3 allows zero-length (empty) header values + frame.headers().add(name, ""); + } - try { - frame.headers().add(name, value); - } catch (IllegalArgumentException e) { - // Name contains NULL or non-ascii characters + name = null; + if (--numHeaders == 0) { + state = State.END_HEADER_BLOCK; + } else { + state = State.READ_NAME_LENGTH; + } + + } else if (length > maxHeaderSize || headerSize > maxHeaderSize - length) { + headerSize = maxHeaderSize + 1; + name = null; + state = State.SKIP_VALUE; + frame.setTruncated(); + } else { + headerSize += length; + state = State.READ_VALUE; + } + break; + + case READ_VALUE: + if (headerBlock.readableBytes() < length) { + return; + } + + byte[] valueBytes = new byte[length]; + headerBlock.readBytes(valueBytes); + + // Add Name/Value pair to headers + int index = 0; + int offset = 0; + + // Value must not start with a NULL character + if (valueBytes[0] == (byte) 0) { + state = State.ERROR; + frame.setInvalid(); + break; + } + + while (index < length) { + while (index < valueBytes.length && valueBytes[index] != (byte) 0) { + index ++; + } + if (index < valueBytes.length) { + // Received NULL character + if (index + 1 == valueBytes.length || valueBytes[index + 1] == (byte) 0) { + // Value field ended with a NULL character or + // received multiple, in-sequence NULL characters. + // Recipients of illegal value fields must issue a stream error + state = State.ERROR; + frame.setInvalid(); + break; + } + } + String value = new String(valueBytes, offset, index - offset, "UTF-8"); + + try { + frame.headers().add(name, value); + } catch (IllegalArgumentException e) { + // Name contains NULL or non-ascii characters + state = State.ERROR; + frame.setInvalid(); + break; + } + index ++; + offset = index; + } + + name = null; + + // If we broke out of the add header loop, break here + if (state == State.ERROR) { + break; + } + + if (--numHeaders == 0) { + state = State.END_HEADER_BLOCK; + } else { + state = State.READ_NAME_LENGTH; + } + break; + + case SKIP_VALUE: + skipLength = Math.min(headerBlock.readableBytes(), length); + headerBlock.skipBytes(skipLength); + length -= skipLength; + + if (length == 0) { + if (--numHeaders == 0) { + state = State.END_HEADER_BLOCK; + } else { + state = State.READ_NAME_LENGTH; + } + } + break; + + case END_HEADER_BLOCK: + state = State.ERROR; frame.setInvalid(); + break; + + case ERROR: + headerBlock.skipBytes(headerBlock.readableBytes()); return; - } - index ++; - offset = index; + + default: + throw new Error("Shouldn't reach here."); } - numHeaders --; - this.headerSize = headerSize; } } @Override - void reset() { + void endHeaderBlock(SpdyHeadersFrame frame) throws Exception { + if (state != State.END_HEADER_BLOCK) { + frame.setInvalid(); + } // Initialize header block decoding fields headerSize = 0; - numHeaders = -1; + name = null; + state = State.READ_NUM_HEADERS; } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoder.java index a4df123043..476aa6bf27 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -26,6 +26,8 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; final class SpdyHeaderBlockZlibDecoder extends SpdyHeaderBlockRawDecoder { private static final int DEFAULT_BUFFER_CAPACITY = 4096; + private static final SpdyProtocolException INVALID_HEADER_BLOCK = + new SpdyProtocolException("Invalid Header Block"); private final Inflater decompressor = new Inflater(); @@ -36,19 +38,22 @@ final class SpdyHeaderBlockZlibDecoder extends SpdyHeaderBlockRawDecoder { } @Override - void decode(ByteBuf encoded, SpdyHeadersFrame frame) throws Exception { - int len = setInput(encoded); + void decode(ByteBuf headerBlock, SpdyHeadersFrame frame) throws Exception { + int len = setInput(headerBlock); int numBytes; do { - numBytes = decompress(encoded.alloc(), frame); + numBytes = decompress(headerBlock.alloc(), frame); } while (numBytes > 0); + // z_stream has an internal 64-bit hold buffer + // it is always capable of consuming the entire input if (decompressor.getRemaining() != 0) { - throw new SpdyProtocolException("client sent extra data beyond headers"); + // we reached the end of the deflate stream + throw INVALID_HEADER_BLOCK; } - encoded.skipBytes(len); + headerBlock.skipBytes(len); } private int setInput(ByteBuf compressed) { @@ -72,7 +77,11 @@ final class SpdyHeaderBlockZlibDecoder extends SpdyHeaderBlockRawDecoder { try { int numBytes = decompressor.inflate(out, off, decompressed.writableBytes()); if (numBytes == 0 && decompressor.needsDictionary()) { - decompressor.setDictionary(SPDY_DICT); + try { + decompressor.setDictionary(SPDY_DICT); + } catch (IllegalArgumentException e) { + throw INVALID_HEADER_BLOCK; + } numBytes = decompressor.inflate(out, off, decompressed.writableBytes()); } if (frame != null) { @@ -95,16 +104,16 @@ final class SpdyHeaderBlockZlibDecoder extends SpdyHeaderBlockRawDecoder { } @Override - void reset() { + void endHeaderBlock(SpdyHeadersFrame frame) throws Exception { + super.endHeaderBlock(frame); releaseBuffer(); - super.reset(); } @Override public void end() { + super.end(); releaseBuffer(); decompressor.end(); - super.end(); } private void releaseBuffer() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java index 071d231535..022071aec1 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2014 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 @@ -129,8 +129,7 @@ public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder { */ protected void addSpdyHandlers(ChannelHandlerContext ctx, SpdyVersion version) { ChannelPipeline pipeline = ctx.pipeline(); - pipeline.addLast("spdyDecoder", new SpdyFrameDecoder(version)); - pipeline.addLast("spdyEncoder", new SpdyFrameEncoder(version)); + pipeline.addLast("spdyFrameCodec", new SpdyFrameCodec(version)); pipeline.addLast("spdySessionHandler", new SpdySessionHandler(version, true)); pipeline.addLast("spdyHttpEncoder", new SpdyHttpEncoder(version)); pipeline.addLast("spdyHttpDecoder", new SpdyHttpDecoder(version, maxSpdyContentLength)); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java index 0500058b11..2b5bcbcec2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2014 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 @@ -15,10 +15,6 @@ */ package io.netty.handler.codec.spdy; -/** - * An {@link Exception} which is thrown when the received frame cannot - * be decoded by the {@link SpdyFrameDecoder}. - */ public class SpdyProtocolException extends Exception { private static final long serialVersionUID = 7870000537743847264L; 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 1212a4257e..1dd3add864 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 @@ -43,6 +43,7 @@ public class SpdySessionHandler private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size private int initialSendWindowSize = DEFAULT_WINDOW_SIZE; private int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE; + private volatile int initialSessionReceiveWindowSize = DEFAULT_WINDOW_SIZE; private final SpdySession spdySession = new SpdySession(initialSendWindowSize, initialReceiveWindowSize); private int lastGoodStreamId; @@ -80,6 +81,19 @@ public class SpdySessionHandler minorVersion = version.getMinorVersion(); } + public void setSessionReceiveWindowSize(int sessionReceiveWindowSize) { + if (sessionReceiveWindowSize < 0) { + throw new IllegalArgumentException("sessionReceiveWindowSize"); + } + // This will not send a window update frame immediately. + // If this value increases the allowed receive window size, + // a WINDOW_UPDATE frame will be sent when only half of the + // session window size remains during data frame processing. + // If this value decreases the allowed receive window size, + // the window will be reduced as data frames are processed. + initialSessionReceiveWindowSize = sessionReceiveWindowSize; + } + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof SpdyDataFrame) { @@ -120,8 +134,8 @@ public class SpdySessionHandler } // Send a WINDOW_UPDATE frame if less than half the session window size remains - if (newSessionWindowSize <= initialReceiveWindowSize / 2) { - int sessionDeltaWindowSize = initialReceiveWindowSize - newSessionWindowSize; + if (newSessionWindowSize <= initialSessionReceiveWindowSize / 2) { + int sessionDeltaWindowSize = initialSessionReceiveWindowSize - newSessionWindowSize; spdySession.updateReceiveWindowSize(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize); SpdyWindowUpdateFrame spdyWindowUpdateFrame = new DefaultSpdyWindowUpdateFrame(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize); diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyFrameDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyFrameDecoderTest.java index 9a2e2e89ab..4da83199fb 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyFrameDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyFrameDecoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 The Netty Project + * Copyright 2014 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 @@ -15,408 +15,1275 @@ */ package io.netty.handler.codec.spdy; -import io.netty.bootstrap.Bootstrap; -import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.NetUtil; -import org.junit.AfterClass; +import io.netty.buffer.Unpooled; +import org.junit.Before; import org.junit.Test; -import java.net.InetSocketAddress; -import java.util.Arrays; -import java.util.List; +import java.util.Random; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.SPDY_HEADER_SIZE; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; import static org.junit.Assert.*; public class SpdyFrameDecoderTest { - private static final EventLoopGroup group = new NioEventLoopGroup(); + private static final Random RANDOM = new Random(); - @AfterClass - public static void destroy() throws Exception { - group.shutdownGracefully().sync(); + private final SpdyFrameDecoderDelegate delegate = createStrictMock(SpdyFrameDecoderDelegate.class); + private SpdyFrameDecoder decoder; + + @Before + public void createDecoder() { + decoder = new SpdyFrameDecoder(SpdyVersion.SPDY_3_1, delegate); + } + + private void encodeDataFrameHeader(ByteBuf buffer, int streamId, byte flags, int length) { + buffer.writeInt(streamId & 0x7FFFFFFF); + buffer.writeByte(flags); + buffer.writeMedium(length); + } + + private void encodeControlFrameHeader(ByteBuf buffer, short type, byte flags, int length) { + buffer.writeShort(0x8000 | SpdyVersion.SPDY_3_1.getVersion()); + buffer.writeShort(type); + buffer.writeByte(flags); + buffer.writeMedium(length); } @Test - public void testTooLargeHeaderNameOnSynStreamRequest() throws Exception { - testTooLargeHeaderNameOnSynStreamRequest(SpdyVersion.SPDY_3_1); - } + public void testSpdyDataFrame() throws Exception { + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + byte flags = 0; + int length = 1024; - private static void testTooLargeHeaderNameOnSynStreamRequest(final SpdyVersion version) throws Exception { - List headerSizes = Arrays.asList(90, 900); - for (final int maxHeaderSize : headerSizes) { // 90 catches the header name, 900 the value - SpdyHeadersFrame frame = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); - addHeader(frame, 100, 1000); - final CaptureHandler captureHandler = new CaptureHandler(); - ServerBootstrap sb = new ServerBootstrap(); - sb.group(group); - sb.channel(NioServerSocketChannel.class); - sb.childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast( - new SpdyFrameDecoder(version, 10000, maxHeaderSize), - new SpdySessionHandler(version, true), - captureHandler); - } - }); - - Bootstrap cb = new Bootstrap(); - cb.group(group); - cb.channel(NioSocketChannel.class); - cb.handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new SpdyFrameEncoder(version)); - } - }); - Channel sc = sb.bind(0).sync().channel(); - int port = ((InetSocketAddress) sc.localAddress()).getPort(); - - Channel cc = cb.connect(NetUtil.LOCALHOST, port).sync().channel(); - - sendAndWaitForFrame(cc, frame, captureHandler); - - assertNotNull("version " + version + ", not null message", - captureHandler.message); - String message = "version " + version + ", should be SpdyHeadersFrame, was " + - captureHandler.message.getClass(); - assertTrue( - message, - captureHandler.message instanceof SpdyHeadersFrame); - SpdyHeadersFrame writtenFrame = (SpdyHeadersFrame) captureHandler.message; - - assertTrue("should be truncated", writtenFrame.isTruncated()); - assertFalse("should not be invalid", writtenFrame.isInvalid()); - - sc.close().sync(); - cc.close().sync(); + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeDataFrameHeader(buf, streamId, flags, length); + for (int i = 0; i < 256; i ++) { + buf.writeInt(RANDOM.nextInt()); } + + delegate.readDataFrame(streamId, false, buf.slice(SPDY_HEADER_SIZE, length)); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } @Test - public void testLargeHeaderNameOnSynStreamRequest() throws Exception { - testLargeHeaderNameOnSynStreamRequest(SpdyVersion.SPDY_3_1); - } + public void testEmptySpdyDataFrame() throws Exception { + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + byte flags = 0; + int length = 0; - private static void testLargeHeaderNameOnSynStreamRequest(final SpdyVersion spdyVersion) throws Exception { - final int maxHeaderSize = 8192; + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeDataFrameHeader(buf, streamId, flags, length); - String expectedName = createString('h', 100); - String expectedValue = createString('v', 5000); - - SpdyHeadersFrame frame = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); - SpdyHeaders headers = frame.headers(); - headers.add(expectedName, expectedValue); - - final CaptureHandler captureHandler = new CaptureHandler(); - final ServerBootstrap sb = new ServerBootstrap(); - Bootstrap cb = new Bootstrap(); - - sb.group(new NioEventLoopGroup(1)); - sb.channel(NioServerSocketChannel.class); - sb.childHandler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ChannelPipeline p = ch.pipeline(); - p.addLast("decoder", new SpdyFrameDecoder(spdyVersion, 10000, maxHeaderSize)); - p.addLast("sessionHandler", new SpdySessionHandler(spdyVersion, true)); - p.addLast("handler", captureHandler); - } - }); - - cb.group(new NioEventLoopGroup(1)); - cb.channel(NioSocketChannel.class); - cb.handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline().addLast("encoder", new SpdyFrameEncoder(spdyVersion)); - } - }); - - Channel sc = sb.bind(new InetSocketAddress(0)).sync().channel(); - int port = ((InetSocketAddress) sc.localAddress()).getPort(); - - ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port)); - assertTrue(ccf.awaitUninterruptibly().isSuccess()); - Channel cc = ccf.channel(); - - sendAndWaitForFrame(cc, frame, captureHandler); - - assertNotNull("version " + spdyVersion.getVersion() + ", not null message", - captureHandler.message); - String message = "version " + spdyVersion.getVersion() + ", should be SpdyHeadersFrame, was " + - captureHandler.message.getClass(); - assertTrue(message, captureHandler.message instanceof SpdyHeadersFrame); - SpdyHeadersFrame writtenFrame = (SpdyHeadersFrame) captureHandler.message; - - assertFalse("should not be truncated", writtenFrame.isTruncated()); - assertFalse("should not be invalid", writtenFrame.isInvalid()); - - String val = writtenFrame.headers().get(expectedName); - assertEquals(expectedValue, val); - - sc.close().sync(); - sb.group().shutdownGracefully(); - cb.group().shutdownGracefully(); + delegate.readDataFrame(streamId, false, Unpooled.EMPTY_BUFFER); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } @Test - public void testZlibHeaders() throws Exception { + public void testLastSpdyDataFrame() throws Exception { + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + byte flags = 0x01; // FLAG_FIN + int length = 0; - SpdyHeadersFrame frame = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); - SpdyHeaders headers = frame.headers(); + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeDataFrameHeader(buf, streamId, flags, length); - headers.add(createString('a', 100), createString('b', 100)); - SpdyHeadersFrame actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(frame.headers().isEmpty()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - actual = roundTrip(frame, 4096); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(frame.headers().isEmpty()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - actual = roundTrip(frame, 128); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(frame.headers().isEmpty()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - headers.add(createString('c', 100), createString('d', 5000)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - headers.add(createString('e', 5000), createString('f', 100)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - headers.add(createString('g', 100), createString('h', 5000)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - headers.clear(); - headers.add(createString('i', 100), createString('j', 5000)); - actual = roundTrip(frame, 4096); - assertTrue("should be truncated", actual.isTruncated()); - assertTrue("headers should be empty", actual.headers().isEmpty()); - - headers.clear(); - headers.add(createString('k', 5000), createString('l', 100)); - actual = roundTrip(frame, 4096); - assertTrue("should be truncated", actual.isTruncated()); - assertTrue("headers should be empty", actual.headers().isEmpty()); - - headers.clear(); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertEquals(1, actual.headers().names().size()); - assertEquals(5, actual.headers().getAll(createString('m', 100)).size()); - - headers.clear(); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertEquals(1, actual.headers().names().size()); - assertEquals(5, actual.headers().getAll(createString('o', 1000)).size()); - - headers.clear(); - headers.add(createString('q', 100), createString('r', 1000)); - headers.add(createString('q', 100), createString('r', 1000)); - headers.add(createString('q', 100), createString('r', 1000)); - headers.add(createString('q', 100), createString('r', 1000)); - headers.add(createString('q', 100), createString('r', 1000)); - actual = roundTrip(frame, 4096); - assertTrue("should be truncated", actual.isTruncated()); - assertEquals(0, actual.headers().names().size()); - - headers.clear(); - headers.add(createString('s', 1000), createString('t', 100)); - headers.add(createString('s', 1000), createString('t', 100)); - headers.add(createString('s', 1000), createString('t', 100)); - headers.add(createString('s', 1000), createString('t', 100)); - headers.add(createString('s', 1000), createString('t', 100)); - actual = roundTrip(frame, 4096); - assertFalse("should be truncated", actual.isTruncated()); - assertEquals(1, actual.headers().names().size()); - assertEquals(5, actual.headers().getAll(createString('s', 1000)).size()); + delegate.readDataFrame(streamId, true, Unpooled.EMPTY_BUFFER); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } @Test - public void testZlibReuseEncoderDecoder() throws Exception { - SpdyHeadersFrame frame = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); - SpdyHeaders headers = frame.headers(); + public void testUnknownSpdyDataFrameFlags() throws Exception { + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + byte flags = (byte) 0xFE; // should ignore any unknown flags + int length = 0; - SpdyHeaderBlockEncoder encoder = SpdyHeaderBlockEncoder.newInstance(SpdyVersion.SPDY_3_1, 6, 15, 8); - SpdyHeaderBlockDecoder decoder = SpdyHeaderBlockDecoder.newInstance(SpdyVersion.SPDY_3_1, 8192); + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeDataFrameHeader(buf, streamId, flags, length); - headers.add(createString('a', 100), createString('b', 100)); - SpdyHeadersFrame actual = roundTrip(encoder, decoder, frame); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - encoder.end(); - decoder.end(); - decoder.reset(); - - headers.clear(); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(frame.headers().isEmpty()); - assertTrue(equals(frame.headers(), actual.headers())); - - encoder.end(); - decoder.end(); - decoder.reset(); - - headers.clear(); - headers.add(createString('e', 5000), createString('f', 100)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - encoder.end(); - decoder.end(); - decoder.reset(); - - headers.clear(); - headers.add(createString('g', 100), createString('h', 5000)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertTrue(equals(frame.headers(), actual.headers())); - - encoder.end(); - decoder.end(); - decoder.reset(); - - headers.clear(); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - headers.add(createString('m', 100), createString('n', 1000)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertEquals(1, actual.headers().names().size()); - assertEquals(5, actual.headers().getAll(createString('m', 100)).size()); - - encoder.end(); - decoder.end(); - decoder.reset(); - - headers.clear(); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - headers.add(createString('o', 1000), createString('p', 100)); - actual = roundTrip(frame, 8192); - assertFalse("should not be truncated", actual.isTruncated()); - assertEquals(1, actual.headers().names().size()); - assertEquals(5, actual.headers().getAll(createString('o', 1000)).size()); + delegate.readDataFrame(streamId, false, Unpooled.EMPTY_BUFFER); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static SpdyHeadersFrame roundTrip(SpdyHeadersFrame frame, int maxHeaderSize) throws Exception { - SpdyHeaderBlockEncoder encoder = SpdyHeaderBlockEncoder.newInstance(SpdyVersion.SPDY_3_1, 6, 15, 8); - SpdyHeaderBlockDecoder decoder = SpdyHeaderBlockDecoder.newInstance(SpdyVersion.SPDY_3_1, maxHeaderSize); - return roundTrip(encoder, decoder, frame); + @Test + public void testIllegalSpdyDataFrameStreamId() throws Exception { + int streamId = 0; // illegal stream identifier + byte flags = 0; + int length = 0; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeDataFrameHeader(buf, streamId, flags, length); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static SpdyHeadersFrame roundTrip(SpdyHeaderBlockEncoder encoder, SpdyHeaderBlockDecoder decoder, - SpdyHeadersFrame frame) throws Exception { - ByteBuf encoded = encoder.encode(frame); + @Test + public void testPipelinedSpdyDataFrames() throws Exception { + int streamId1 = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int streamId2 = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + byte flags = 0; + int length = 0; - SpdyHeadersFrame actual = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); + ByteBuf buf = Unpooled.buffer(2 * (SPDY_HEADER_SIZE + length)); + encodeDataFrameHeader(buf, streamId1, flags, length); + encodeDataFrameHeader(buf, streamId2, flags, length); - decoder.decode(encoded, actual); - return actual; + delegate.readDataFrame(streamId1, false, Unpooled.EMPTY_BUFFER); + delegate.readDataFrame(streamId2, false, Unpooled.EMPTY_BUFFER); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static boolean equals(SpdyHeaders h1, SpdyHeaders h2) { - if (!h1.names().equals(h2.names())) { - return false; + @Test + public void testSpdySynStreamFrame() throws Exception { + short type = 1; + byte flags = 0; + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testLastSpdySynStreamFrame() throws Exception { + short type = 1; + byte flags = 0x01; // FLAG_FIN + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, true, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnidirectionalSpdySynStreamFrame() throws Exception { + short type = 1; + byte flags = 0x02; // FLAG_UNIDIRECTIONAL + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, true); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIndependentSpdySynStreamFrame() throws Exception { + short type = 1; + byte flags = 0; + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = 0; // independent of all other streams + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdySynStreamFrameFlags() throws Exception { + short type = 1; + byte flags = (byte) 0xFC; // undefined flags + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testReservedSpdySynStreamFrameBits() throws Exception { + short type = 1; + byte flags = 0; + int length = 10; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId | 0x80000000); // should ignore reserved bit + buf.writeInt(associatedToStreamId | 0x80000000); // should ignore reserved bit + buf.writeByte((priority << 5) | 0x1F); // should ignore reserved bits + buf.writeByte(0xFF); // should ignore reserved bits + + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdySynStreamFrameLength() throws Exception { + short type = 1; + byte flags = 0; + int length = 8; // invalid length + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIllegalSpdySynStreamFrameStreamId() throws Exception { + short type = 1; + byte flags = 0; + int length = 10; + int streamId = 0; // invalid stream identifier + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySynStreamFrameHeaderBlock() throws Exception { + short type = 1; + byte flags = 0; + int length = 10; + int headerBlockLength = 1024; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int associatedToStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + byte priority = (byte) (RANDOM.nextInt() & 0x07); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length + headerBlockLength); + encodeControlFrameHeader(buf, type, flags, length + headerBlockLength); + buf.writeInt(streamId); + buf.writeInt(associatedToStreamId); + buf.writeByte(priority << 5); + buf.writeByte(0); + + ByteBuf headerBlock = Unpooled.buffer(headerBlockLength); + for (int i = 0; i < 256; i ++) { + headerBlock.writeInt(RANDOM.nextInt()); } - for (String name : h1.names()) { - if (!h1.getAll(name).equals(h2.getAll(name))) { - return false; - } - } - return true; + delegate.readSynStreamFrame(streamId, associatedToStreamId, priority, false, false); + delegate.readHeaderBlock(headerBlock.duplicate()); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + decoder.decode(headerBlock); + verify(delegate); + assertFalse(buf.isReadable()); + assertFalse(headerBlock.isReadable()); } - private static void sendAndWaitForFrame(Channel cc, SpdyFrame frame, CaptureHandler handler) { - cc.writeAndFlush(frame); - long theFuture = System.currentTimeMillis() + 3000; - while (handler.message == null && System.currentTimeMillis() < theFuture) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - // Ignore. - } - } + @Test + public void testSpdySynReplyFrame() throws Exception { + short type = 2; + byte flags = 0; + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readSynReplyFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static void addHeader(SpdyHeadersFrame frame, int headerNameSize, int headerValueSize) { - frame.headers().add("k", "v"); - String headerName = createString('h', headerNameSize); - String headerValue = createString('h', headerValueSize); - frame.headers().add(headerName, headerValue); + @Test + public void testLastSpdySynReplyFrame() throws Exception { + short type = 2; + byte flags = 0x01; // FLAG_FIN + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readSynReplyFrame(streamId, true); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static String createString(char c, int rep) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < rep; i++) { - sb.append(c); - } - return sb.toString(); + @Test + public void testUnknownSpdySynReplyFrameFlags() throws Exception { + short type = 2; + byte flags = (byte) 0xFE; // undefined flags + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readSynReplyFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } - private static class CaptureHandler extends ChannelInboundHandlerAdapter { - public volatile Object message; + @Test + public void testReservedSpdySynReplyFrameBits() throws Exception { + short type = 2; + byte flags = 0; + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; - @Override - public void channelRead(ChannelHandlerContext ctx, Object m) throws Exception { - message = m; + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId | 0x80000000); // should ignore reserved bit + + delegate.readSynReplyFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdySynReplyFrameLength() throws Exception { + short type = 2; + byte flags = 0; + int length = 0; // invalid length + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIllegalSpdySynReplyFrameStreamId() throws Exception { + short type = 2; + byte flags = 0; + int length = 4; + int streamId = 0; // invalid stream identifier + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySynReplyFrameHeaderBlock() throws Exception { + short type = 2; + byte flags = 0; + int length = 4; + int headerBlockLength = 1024; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length + headerBlockLength); + encodeControlFrameHeader(buf, type, flags, length + headerBlockLength); + buf.writeInt(streamId); + + ByteBuf headerBlock = Unpooled.buffer(headerBlockLength); + for (int i = 0; i < 256; i ++) { + headerBlock.writeInt(RANDOM.nextInt()); } - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - cause.printStackTrace(); - message = cause; + delegate.readSynReplyFrame(streamId, false); + delegate.readHeaderBlock(headerBlock.duplicate()); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + decoder.decode(headerBlock); + verify(delegate); + assertFalse(buf.isReadable()); + assertFalse(headerBlock.isReadable()); + } + + @Test + public void testSpdyRstStreamFrame() throws Exception { + short type = 3; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(statusCode); + + delegate.readRstStreamFrame(streamId, statusCode); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testReservedSpdyRstStreamFrameBits() throws Exception { + short type = 3; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId | 0x80000000); // should ignore reserved bit + buf.writeInt(statusCode); + + delegate.readRstStreamFrame(streamId, statusCode); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyRstStreamFrameFlags() throws Exception { + short type = 3; + byte flags = (byte) 0xFF; // invalid flags + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(statusCode); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyRstStreamFrameLength() throws Exception { + short type = 3; + byte flags = 0; + int length = 12; // invalid length + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(statusCode); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIllegalSpdyRstStreamFrameStreamId() throws Exception { + short type = 3; + byte flags = 0; + int length = 8; + int streamId = 0; // invalid stream identifier + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(statusCode); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIllegalSpdyRstStreamFrameStatusCode() throws Exception { + short type = 3; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + int statusCode = 0; // invalid status code + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(statusCode); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySettingsFrame() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 2; + int length = 8 * numSettings + 4; + byte idFlags = 0; + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); } + + delegate.readSettingsFrame(false); + delegate.readSetting(id, value, false, false); + expectLastCall().times(numSettings); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testEmptySpdySettingsFrame() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 0; + int length = 8 * numSettings + 4; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + + delegate.readSettingsFrame(false); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySettingsFrameClearFlag() throws Exception { + short type = 4; + byte flags = 0x01; // FLAG_SETTINGS_CLEAR_SETTINGS + int numSettings = 0; + int length = 8 * numSettings + 4; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + + delegate.readSettingsFrame(true); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySettingsPersistValues() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 1; + int length = 8 * numSettings + 4; + byte idFlags = 0x01; // FLAG_SETTINGS_PERSIST_VALUE + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); + } + + delegate.readSettingsFrame(false); + delegate.readSetting(id, value, true, false); + expectLastCall().times(numSettings); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdySettingsPersistedValues() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 1; + int length = 8 * numSettings + 4; + byte idFlags = 0x02; // FLAG_SETTINGS_PERSISTED + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); + } + + delegate.readSettingsFrame(false); + delegate.readSetting(id, value, false, true); + expectLastCall().times(numSettings); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdySettingsFrameFlags() throws Exception { + short type = 4; + byte flags = (byte) 0xFE; // undefined flags + int numSettings = 0; + int length = 8 * numSettings + 4; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + + delegate.readSettingsFrame(false); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdySettingsFlags() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 1; + int length = 8 * numSettings + 4; + byte idFlags = (byte) 0xFC; // undefined flags + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); + } + + delegate.readSettingsFrame(false); + delegate.readSetting(id, value, false, false); + expectLastCall().times(numSettings); + delegate.readSettingsEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdySettingsFrameLength() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 2; + int length = 8 * numSettings + 8; // invalid length + byte idFlags = 0; + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(numSettings); + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); + } + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdySettingsFrameNumSettings() throws Exception { + short type = 4; + byte flags = 0; + int numSettings = 2; + int length = 8 * numSettings + 4; + byte idFlags = 0; + int id = RANDOM.nextInt() & 0x00FFFFFF; + int value = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(0); // invalid num_settings + for (int i = 0; i < numSettings; i++) { + buf.writeByte(idFlags); + buf.writeMedium(id); + buf.writeInt(value); + } + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testDiscardUnknownFrame() throws Exception { + short type = 5; + byte flags = (byte) 0xFF; + int length = 8; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeLong(RANDOM.nextLong()); + + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testDiscardUnknownEmptyFrame() throws Exception { + short type = 5; + byte flags = (byte) 0xFF; + int length = 0; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testProgressivelyDiscardUnknownEmptyFrame() throws Exception { + short type = 5; + byte flags = (byte) 0xFF; + int segment = 4; + int length = 2 * segment; + + ByteBuf header = Unpooled.buffer(SPDY_HEADER_SIZE); + ByteBuf segment1 = Unpooled.buffer(segment); + ByteBuf segment2 = Unpooled.buffer(segment); + encodeControlFrameHeader(header, type, flags, length); + segment1.writeInt(RANDOM.nextInt()); + segment2.writeInt(RANDOM.nextInt()); + + replay(delegate); + decoder.decode(header); + decoder.decode(segment1); + decoder.decode(segment2); + verify(delegate); + assertFalse(header.isReadable()); + assertFalse(segment1.isReadable()); + assertFalse(segment2.isReadable()); + } + + @Test + public void testSpdyPingFrame() throws Exception { + short type = 6; + byte flags = 0; + int length = 4; + int id = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(id); + + delegate.readPingFrame(id); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdyPingFrameFlags() throws Exception { + short type = 6; + byte flags = (byte) 0xFF; // undefined flags + int length = 4; + int id = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(id); + + delegate.readPingFrame(id); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyPingFrameLength() throws Exception { + short type = 6; + byte flags = 0; + int length = 8; // invalid length + int id = RANDOM.nextInt(); + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(id); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdyGoAwayFrame() throws Exception { + short type = 7; + byte flags = 0; + int length = 8; + int lastGoodStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(lastGoodStreamId); + buf.writeInt(statusCode); + + delegate.readGoAwayFrame(lastGoodStreamId, statusCode); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdyGoAwayFrameFlags() throws Exception { + short type = 7; + byte flags = (byte) 0xFF; // undefined flags + int length = 8; + int lastGoodStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(lastGoodStreamId); + buf.writeInt(statusCode); + + delegate.readGoAwayFrame(lastGoodStreamId, statusCode); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testReservedSpdyGoAwayFrameBits() throws Exception { + short type = 7; + byte flags = 0; + int length = 8; + int lastGoodStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(lastGoodStreamId | 0x80000000); // should ignore reserved bit + buf.writeInt(statusCode); + + delegate.readGoAwayFrame(lastGoodStreamId, statusCode); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyGoAwayFrameLength() throws Exception { + short type = 7; + byte flags = 0; + int length = 12; // invalid length + int lastGoodStreamId = RANDOM.nextInt() & 0x7FFFFFFF; + int statusCode = RANDOM.nextInt() | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(lastGoodStreamId); + buf.writeInt(statusCode); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdyHeadersFrame() throws Exception { + short type = 8; + byte flags = 0; + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readHeadersFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testLastSpdyHeadersFrame() throws Exception { + short type = 8; + byte flags = 0x01; // FLAG_FIN + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readHeadersFrame(streamId, true); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdyHeadersFrameFlags() throws Exception { + short type = 8; + byte flags = (byte) 0xFE; // undefined flags + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readHeadersFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testReservedSpdyHeadersFrameBits() throws Exception { + short type = 8; + byte flags = 0; + int length = 4; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId | 0x80000000); // should ignore reserved bit + + delegate.readHeadersFrame(streamId, false); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyHeadersFrameLength() throws Exception { + short type = 8; + byte flags = 0; + int length = 0; // invalid length + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyHeadersFrameStreamId() throws Exception { + short type = 8; + byte flags = 0; + int length = 4; + int streamId = 0; // invalid stream identifier + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testSpdyHeadersFrameHeaderBlock() throws Exception { + short type = 8; + byte flags = 0; + int length = 4; + int headerBlockLength = 1024; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length + headerBlockLength); + encodeControlFrameHeader(buf, type, flags, length + headerBlockLength); + buf.writeInt(streamId); + + ByteBuf headerBlock = Unpooled.buffer(headerBlockLength); + for (int i = 0; i < 256; i ++) { + headerBlock.writeInt(RANDOM.nextInt()); + } + + delegate.readHeadersFrame(streamId, false); + delegate.readHeaderBlock(headerBlock.duplicate()); + delegate.readHeaderBlockEnd(); + replay(delegate); + decoder.decode(buf); + decoder.decode(headerBlock); + verify(delegate); + assertFalse(buf.isReadable()); + assertFalse(headerBlock.isReadable()); + } + + @Test + public void testSpdyWindowUpdateFrame() throws Exception { + short type = 9; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF; + int deltaWindowSize = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(deltaWindowSize); + + delegate.readWindowUpdateFrame(streamId, deltaWindowSize); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testUnknownSpdyWindowUpdateFrameFlags() throws Exception { + short type = 9; + byte flags = (byte) 0xFF; // undefined flags + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF; + int deltaWindowSize = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(deltaWindowSize); + + delegate.readWindowUpdateFrame(streamId, deltaWindowSize); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testReservedSpdyWindowUpdateFrameBits() throws Exception { + short type = 9; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF; + int deltaWindowSize = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId | 0x80000000); // should ignore reserved bit + buf.writeInt(deltaWindowSize | 0x80000000); // should ignore reserved bit + + delegate.readWindowUpdateFrame(streamId, deltaWindowSize); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testInvalidSpdyWindowUpdateFrameLength() throws Exception { + short type = 9; + byte flags = 0; + int length = 12; // invalid length + int streamId = RANDOM.nextInt() & 0x7FFFFFFF; + int deltaWindowSize = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(deltaWindowSize); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); + } + + @Test + public void testIllegalSpdyWindowUpdateFrameDeltaWindowSize() throws Exception { + short type = 9; + byte flags = 0; + int length = 8; + int streamId = RANDOM.nextInt() & 0x7FFFFFFF; + int deltaWindowSize = 0; // invalid delta window size + + ByteBuf buf = Unpooled.buffer(SPDY_HEADER_SIZE + length); + encodeControlFrameHeader(buf, type, flags, length); + buf.writeInt(streamId); + buf.writeInt(deltaWindowSize); + + delegate.readFrameError((String) anyObject()); + replay(delegate); + decoder.decode(buf); + verify(delegate); + assertFalse(buf.isReadable()); } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoderTest.java new file mode 100644 index 0000000000..1436dfa534 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoderTest.java @@ -0,0 +1,485 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SpdyHeaderBlockRawDecoderTest { + + private static final int maxHeaderSize = 16; + + private static final String name = "name"; + private static final String value = "value"; + private static final byte[] nameBytes = name.getBytes(); + private static final byte[] valueBytes = value.getBytes(); + + private SpdyHeaderBlockRawDecoder decoder; + private SpdyHeadersFrame frame; + + @Before + public void setUp() { + decoder = new SpdyHeaderBlockRawDecoder(SpdyVersion.SPDY_3_1, maxHeaderSize); + frame = new DefaultSpdyHeadersFrame(1); + } + + @Test + public void testEmptyHeaderBlock() throws Exception { + ByteBuf headerBlock = Unpooled.EMPTY_BUFFER; + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testZeroNameValuePairs() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(4); + headerBlock.writeInt(0); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testNegativeNameValuePairs() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(4); + headerBlock.writeInt(-1); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testOneNameValuePair() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(21); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testMissingNameLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(4); + headerBlock.writeInt(1); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testZeroNameLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(8); + headerBlock.writeInt(1); + headerBlock.writeInt(0); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testNegativeNameLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(8); + headerBlock.writeInt(1); + headerBlock.writeInt(-1); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testMissingName() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(8); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testIllegalNameOnlyNull() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(18); + headerBlock.writeInt(1); + headerBlock.writeInt(1); + headerBlock.writeByte(0); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testMissingValueLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(12); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testZeroValueLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(16); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(0); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals("", frame.headers().get(name)); + } + + @Test + public void testNegativeValueLength() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(16); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(-1); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testMissingValue() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(16); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testIllegalValueOnlyNull() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(17); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(1); + headerBlock.writeByte(0); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testIllegalValueStartsWithNull() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(22); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(6); + headerBlock.writeByte(0); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testIllegalValueEndsWithNull() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(22); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(6); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testMultipleValues() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(27); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(11); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(2, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().getAll(name).get(0)); + assertEquals(value, frame.headers().getAll(name).get(1)); + } + + @Test + public void testMultipleValuesEndsWithNull() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(28); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(12); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testIllegalValueMultipleNulls() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(28); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(12); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + headerBlock.writeByte(0); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testMissingNextNameValuePair() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(21); + headerBlock.writeInt(2); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testMultipleNames() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(38); + headerBlock.writeInt(2); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testExtraData() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(22); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + decoder.decode(headerBlock, frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testMultipleDecodes() throws Exception { + ByteBuf numHeaders = Unpooled.buffer(4); + numHeaders.writeInt(1); + + ByteBuf nameBlock = Unpooled.buffer(8); + nameBlock.writeInt(4); + nameBlock.writeBytes(nameBytes); + + ByteBuf valueBlock = Unpooled.buffer(9); + valueBlock.writeInt(5); + valueBlock.writeBytes(valueBytes); + + decoder.decode(numHeaders, frame); + decoder.decode(nameBlock, frame); + decoder.decode(valueBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(numHeaders.isReadable()); + assertFalse(nameBlock.isReadable()); + assertFalse(valueBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testContinueAfterInvalidHeaders() throws Exception { + ByteBuf numHeaders = Unpooled.buffer(4); + numHeaders.writeInt(1); + + ByteBuf nameBlock = Unpooled.buffer(8); + nameBlock.writeInt(4); + nameBlock.writeBytes(nameBytes); + + ByteBuf valueBlock = Unpooled.buffer(9); + valueBlock.writeInt(5); + valueBlock.writeBytes(valueBytes); + + decoder.decode(numHeaders, frame); + decoder.decode(nameBlock, frame); + frame.setInvalid(); + decoder.decode(valueBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(numHeaders.isReadable()); + assertFalse(nameBlock.isReadable()); + assertFalse(valueBlock.isReadable()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testTruncatedHeaderName() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(maxHeaderSize + 18); + headerBlock.writeInt(1); + headerBlock.writeInt(maxHeaderSize + 1); + for (int i = 0; i < maxHeaderSize + 1; i++) { + headerBlock.writeByte('a'); + } + headerBlock.writeInt(5); + headerBlock.writeBytes(valueBytes); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isTruncated()); + assertFalse(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } + + @Test + public void testTruncatedHeaderValue() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(maxHeaderSize + 13); + headerBlock.writeInt(1); + headerBlock.writeInt(4); + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(13); + for (int i = 0; i < maxHeaderSize - 3; i++) { + headerBlock.writeByte('a'); + } + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertTrue(frame.isTruncated()); + assertFalse(frame.isInvalid()); + assertEquals(0, frame.headers().names().size()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoderTest.java new file mode 100644 index 0000000000..49e049d6dc --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecoderTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SpdyHeaderBlockZlibDecoderTest { + + // zlib header indicating 32K window size fastest deflate algorithm with SPDY dictionary + private static final byte[] zlibHeader = {0x78, 0x3f, (byte) 0xe3, (byte) 0xc6, (byte) 0xa7, (byte) 0xc2}; + private static final byte[] zlibSyncFlush = {0x00, 0x00, 0x00, (byte) 0xff, (byte) 0xff}; + + private static final int maxHeaderSize = 8192; + + private static final String name = "name"; + private static final String value = "value"; + private static final byte[] nameBytes = name.getBytes(); + private static final byte[] valueBytes = value.getBytes(); + + private SpdyHeaderBlockZlibDecoder decoder; + private SpdyHeadersFrame frame; + + @Before + public void setUp() { + decoder = new SpdyHeaderBlockZlibDecoder(SpdyVersion.SPDY_3_1, maxHeaderSize); + frame = new DefaultSpdyHeadersFrame(1); + } + + @Test + public void testHeaderBlock() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(37); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x15); // little-endian length (21) + headerBlock.writeByte(0x00); // little-endian length (21) + headerBlock.writeByte(0xea); // one's compliment of length + headerBlock.writeByte(0xff); // one's compliment of length + headerBlock.writeInt(1); // number of Name/Value pairs + headerBlock.writeInt(4); // length of name + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); // length of value + headerBlock.writeBytes(valueBytes); + headerBlock.writeBytes(zlibSyncFlush); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testHeaderBlockMultipleDecodes() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(37); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x15); // little-endian length (21) + headerBlock.writeByte(0x00); // little-endian length (21) + headerBlock.writeByte(0xea); // one's compliment of length + headerBlock.writeByte(0xff); // one's compliment of length + headerBlock.writeInt(1); // number of Name/Value pairs + headerBlock.writeInt(4); // length of name + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); // length of value + headerBlock.writeBytes(valueBytes); + headerBlock.writeBytes(zlibSyncFlush); + + int readableBytes = headerBlock.readableBytes(); + for (int i = 0; i < readableBytes; i++) { + ByteBuf headerBlockSegment = headerBlock.slice(i, 1); + decoder.decode(headerBlockSegment, frame); + } + decoder.endHeaderBlock(frame); + + assertFalse(frame.isInvalid()); + assertEquals(1, frame.headers().names().size()); + assertTrue(frame.headers().contains(name)); + assertEquals(1, frame.headers().getAll(name).size()); + assertEquals(value, frame.headers().get(name)); + } + + @Test + public void testLargeHeaderName() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(8220); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x0c); // little-endian length (8204) + headerBlock.writeByte(0x20); // little-endian length (8204) + headerBlock.writeByte(0xf3); // one's compliment of length + headerBlock.writeByte(0xdf); // one's compliment of length + headerBlock.writeInt(1); // number of Name/Value pairs + headerBlock.writeInt(8192); // length of name + for (int i = 0; i < 8192; i++) { + headerBlock.writeByte('n'); + } + headerBlock.writeInt(0); // length of value + headerBlock.writeBytes(zlibSyncFlush); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertFalse(frame.isTruncated()); + assertEquals(1, frame.headers().names().size()); + } + + @Test + public void testLargeHeaderValue() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(8220); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x0c); // little-endian length (8204) + headerBlock.writeByte(0x20); // little-endian length (8204) + headerBlock.writeByte(0xf3); // one's compliment of length + headerBlock.writeByte(0xdf); // one's compliment of length + headerBlock.writeInt(1); // number of Name/Value pairs + headerBlock.writeInt(1); // length of name + headerBlock.writeByte('n'); + headerBlock.writeInt(8191); // length of value + for (int i = 0; i < 8191; i++) { + headerBlock.writeByte('v'); + } + headerBlock.writeBytes(zlibSyncFlush); + decoder.decode(headerBlock, frame); + decoder.endHeaderBlock(frame); + + assertFalse(headerBlock.isReadable()); + assertFalse(frame.isInvalid()); + assertFalse(frame.isTruncated()); + assertEquals(1, frame.headers().names().size()); + assertEquals(8191, frame.headers().get("n").length()); + } + + @Test(expected = SpdyProtocolException.class) + public void testHeaderBlockExtraData() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(37); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x15); // little-endian length (21) + headerBlock.writeByte(0x00); // little-endian length (21) + headerBlock.writeByte(0xea); // one's compliment of length + headerBlock.writeByte(0xff); // one's compliment of length + headerBlock.writeInt(1); // number of Name/Value pairs + headerBlock.writeInt(4); // length of name + headerBlock.writeBytes(nameBytes); + headerBlock.writeInt(5); // length of value + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0x19); // adler-32 checksum + headerBlock.writeByte(0xa5); // adler-32 checksum + headerBlock.writeByte(0x03); // adler-32 checksum + headerBlock.writeByte(0xc9); // adler-32 checksum + headerBlock.writeByte(0); // Data following zlib stream + decoder.decode(headerBlock, frame); + } + + @Test(expected = SpdyProtocolException.class) + public void testHeaderBlockInvalidDictionary() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(7); + headerBlock.writeByte(0x78); + headerBlock.writeByte(0x3f); + headerBlock.writeByte(0x01); // Unknown dictionary + headerBlock.writeByte(0x02); // Unknown dictionary + headerBlock.writeByte(0x03); // Unknown dictionary + headerBlock.writeByte(0x04); // Unknown dictionary + headerBlock.writeByte(0); // Non-compressed block + decoder.decode(headerBlock, frame); + } + + @Test(expected = SpdyProtocolException.class) + public void testHeaderBlockInvalidDeflateBlock() throws Exception { + ByteBuf headerBlock = Unpooled.buffer(11); + headerBlock.writeBytes(zlibHeader); + headerBlock.writeByte(0); // Non-compressed block + headerBlock.writeByte(0x00); // little-endian length (0) + headerBlock.writeByte(0x00); // little-endian length (0) + headerBlock.writeByte(0x00); // invalid one's compliment + headerBlock.writeByte(0x00); // invalid one's compliment + decoder.decode(headerBlock, frame); + } +} 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 3b2cab3acb..0988375514 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 @@ -43,8 +43,8 @@ public class SpdySessionHandlerTest { assertNotNull(msg); assertTrue(msg instanceof SpdyDataFrame); SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; - assertEquals(spdyDataFrame.getStreamId(), streamId); - assertEquals(spdyDataFrame.isLast(), last); + assertEquals(streamId, spdyDataFrame.getStreamId()); + assertEquals(last, spdyDataFrame.isLast()); } private static void assertSynReply(Object msg, int streamId, boolean last, SpdyHeaders headers) { @@ -57,30 +57,30 @@ public class SpdySessionHandlerTest { assertNotNull(msg); assertTrue(msg instanceof SpdyRstStreamFrame); SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; - assertEquals(spdyRstStreamFrame.getStreamId(), streamId); - assertEquals(spdyRstStreamFrame.getStatus(), status); + assertEquals(streamId, spdyRstStreamFrame.getStreamId()); + assertEquals(status, spdyRstStreamFrame.getStatus()); } private static void assertPing(Object msg, int id) { assertNotNull(msg); assertTrue(msg instanceof SpdyPingFrame); SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; - assertEquals(spdyPingFrame.getId(), id); + assertEquals(id, spdyPingFrame.getId()); } private static void assertGoAway(Object msg, int lastGoodStreamId) { assertNotNull(msg); assertTrue(msg instanceof SpdyGoAwayFrame); SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; - assertEquals(spdyGoAwayFrame.getLastGoodStreamId(), lastGoodStreamId); + assertEquals(lastGoodStreamId, spdyGoAwayFrame.getLastGoodStreamId()); } private static void assertHeaders(Object msg, int streamId, boolean last, SpdyHeaders headers) { assertNotNull(msg); assertTrue(msg instanceof SpdyHeadersFrame); SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; - assertEquals(spdyHeadersFrame.getStreamId(), streamId); - assertEquals(spdyHeadersFrame.isLast(), last); + assertEquals(streamId, spdyHeadersFrame.getStreamId()); + assertEquals(last, spdyHeadersFrame.isLast()); for (String name: headers.names()) { List expectedValues = headers.getAll(name); List receivedValues = spdyHeadersFrame.headers().getAll(name); diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClientInitializer.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClientInitializer.java index 545cedb193..ffc6d75fd5 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyClientInitializer.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClientInitializer.java @@ -19,8 +19,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.example.securechat.SecureChatSslContextFactory; -import io.netty.handler.codec.spdy.SpdyFrameDecoder; -import io.netty.handler.codec.spdy.SpdyFrameEncoder; +import io.netty.handler.codec.spdy.SpdyFrameCodec; import io.netty.handler.codec.spdy.SpdyHttpDecoder; import io.netty.handler.codec.spdy.SpdyHttpEncoder; import io.netty.handler.codec.spdy.SpdySessionHandler; @@ -52,8 +51,7 @@ public class SpdyClientInitializer extends ChannelInitializer { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("ssl", new SslHandler(engine)); - pipeline.addLast("spdyEncoder", new SpdyFrameEncoder(SPDY_3_1)); - pipeline.addLast("spdyDecoder", new SpdyFrameDecoder(SPDY_3_1)); + pipeline.addLast("spdyFrameCodec", new SpdyFrameCodec(SPDY_3_1)); pipeline.addLast("spdyFrameLogger", new SpdyFrameLogger(INFO)); pipeline.addLast("spdySessionHandler", new SpdySessionHandler(SPDY_3_1, false)); pipeline.addLast("spdyHttpEncoder", new SpdyHttpEncoder(SPDY_3_1)); diff --git a/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketSpdyEchoTest.java b/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketSpdyEchoTest.java index 163ce24e83..8e5ef76739 100644 --- a/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketSpdyEchoTest.java +++ b/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketSpdyEchoTest.java @@ -25,8 +25,7 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.spdy.SpdyFrameDecoder; -import io.netty.handler.codec.spdy.SpdyFrameEncoder; +import io.netty.handler.codec.spdy.SpdyFrameCodec; import io.netty.handler.codec.spdy.SpdyVersion; import io.netty.util.NetUtil; import org.junit.Test; @@ -184,8 +183,7 @@ public class SocketSpdyEchoTest extends AbstractSocketTest { @Override public void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast( - new SpdyFrameDecoder(version), - new SpdyFrameEncoder(version), + new SpdyFrameCodec(version), sh); } });