diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpResponseStatus.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpResponseStatus.java index 5dcc35da19..6a5b8cbe7d 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpResponseStatus.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpResponseStatus.java @@ -233,6 +233,12 @@ public class HttpResponseStatus implements Comparable { */ public static final HttpResponseStatus UPGRADE_REQUIRED = new HttpResponseStatus(426, "Upgrade Required"); + /** + * 431 Request Header Fields Too Large (RFC6585) + */ + public static final HttpResponseStatus REQUEST_HEADER_FIELDS_TOO_LARGE = + new HttpResponseStatus(431, "Request Header Fields Too Large"); + /** * 500 Internal Server Error */ diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java index 5f3f34062e..1ccfd7d17f 100644 --- a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java @@ -28,6 +28,7 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame implements SpdyHeadersFrame { private boolean invalid; + private boolean truncated; private final SpdyHeaders headers = new SpdyHeaders(); /** @@ -47,6 +48,14 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame invalid = true; } + public boolean isTruncated() { + return truncated; + } + + public void setTruncated() { + truncated = true; + } + public void addHeader(final String name, final Object value) { headers.addHeader(name, value); } diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java index 4cd156fa26..9622dec0e9 100644 --- a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -243,7 +243,8 @@ public class SpdyFrameDecoder extends FrameDecoder { return null; } - if (spdyHeadersFrame != null && spdyHeadersFrame.isInvalid()) { + if (spdyHeadersFrame != null && + (spdyHeadersFrame.isInvalid() || spdyHeadersFrame.isTruncated())) { Object frame = spdyHeadersFrame; spdyHeadersFrame = null; if (length == 0) { diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java index ad306756ab..8e4e94c54d 100644 --- a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java @@ -94,8 +94,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { } headerSize += nameLength; if (headerSize > maxHeaderSize) { - throw new TooLongFrameException( - "Header block exceeds " + maxHeaderSize); + frame.setTruncated(); + return; } // Try to read name @@ -143,8 +143,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { headerSize += valueLength; if (headerSize > maxHeaderSize) { - throw new TooLongFrameException( - "Header block exceeds " + maxHeaderSize); + frame.setTruncated(); + return; } // Try to read value diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java index 34653ce1ca..4cd56e7f66 100644 --- a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java @@ -35,6 +35,17 @@ public interface SpdyHeadersFrame extends SpdyStreamFrame { */ void setInvalid(); + /** + * Returns {@code true} if this header block has been truncated due to + * length restrictions. + */ + boolean isTruncated(); + + /** + * Mark this header block as truncated. + */ + void setTruncated(); + /** * Returns the header value with the specified header name. If there is * more than one header value for the specified header name, the first diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHttpDecoder.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHttpDecoder.java index 65f1d53efc..e20cd8f182 100644 --- a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHttpDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHttpDecoder.java @@ -126,6 +126,14 @@ public class SpdyHttpDecoder extends OneToOneDecoder { Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame); } + // If a client receives a response with a truncated header block, + // reply with a RST_STREAM with error code INTERNAL_ERROR. + if (spdySynStreamFrame.isTruncated()) { + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR); + Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame); + } + try { HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame); @@ -149,6 +157,19 @@ public class SpdyHttpDecoder extends OneToOneDecoder { } } else { // SYN_STREAM frames initiated by the client are HTTP requests + + // If a client sends a request with a truncated header block, the server must + // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply. + if (spdySynStreamFrame.isTruncated()) { + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); + spdySynReplyFrame.setLast(true); + SpdyHeaders.setStatus(spdyVersion, + spdySynReplyFrame, + HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE); + SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0); + Channels.write(ctx, Channels.future(channel), spdySynReplyFrame); + } + try { HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame); @@ -178,6 +199,14 @@ public class SpdyHttpDecoder extends OneToOneDecoder { SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; int streamId = spdySynReplyFrame.getStreamId(); + // If a client receives a SYN_REPLY with a truncated header block, + // reply with a RST_STREAM frame with error code INTERNAL_ERROR. + if (spdySynReplyFrame.isTruncated()) { + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR); + Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame); + } + try { HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame); @@ -210,8 +239,11 @@ public class SpdyHttpDecoder extends OneToOneDecoder { return null; } - for (Map.Entry e: spdyHeadersFrame.getHeaders()) { - httpMessage.addHeader(e.getKey(), e.getValue()); + // Ignore trailers in a truncated HEADERS frame. + if (!spdyHeadersFrame.isTruncated()) { + for (Map.Entry e : spdyHeadersFrame.getHeaders()) { + httpMessage.addHeader(e.getKey(), e.getValue()); + } } if (spdyHeadersFrame.isLast()) { diff --git a/src/test/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoderTest.java b/src/test/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoderTest.java new file mode 100644 index 0000000000..36e7ce2d50 --- /dev/null +++ b/src/test/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoderTest.java @@ -0,0 +1,120 @@ +package org.jboss.netty.handler.codec.spdy; + +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.*; +import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; +import org.jboss.netty.util.TestUtil; +import org.junit.Test; + +import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*; +import static org.junit.Assert.*; + +public class SpdyFrameDecoderTest { + + @Test + public void testTooLargeHeaderNameOnSynStreamRequest() throws Exception { + for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version++) { + List headerSizes = Arrays.asList(90, 900); + for (int maxHeaderSize : headerSizes) { // 90 catches the header name, 900 the value + SpdyHeadersFrame frame = new DefaultSpdySynStreamFrame(1, 0, (byte) 0); + addHeader(frame, 100, 1000); + CaptureHandler captureHandler = new CaptureHandler(); + ServerBootstrap sb = new ServerBootstrap( + newServerSocketChannelFactory(Executors.newCachedThreadPool())); + ClientBootstrap cb = new ClientBootstrap( + newClientSocketChannelFactory(Executors.newCachedThreadPool())); + + sb.getPipeline().addLast("decoder", new SpdyFrameDecoder(version, 10000, maxHeaderSize)); + sb.getPipeline().addLast("sessionHandler", new SpdySessionHandler(version, true)); + sb.getPipeline().addLast("handler", captureHandler); + + cb.getPipeline().addLast("encoder", new SpdyFrameEncoder(version)); + + Channel sc = sb.bind(new InetSocketAddress(0)); + int port = ((InetSocketAddress) sc.getLocalAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(TestUtil.getLocalHost(), port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + Channel cc = ccf.getChannel(); + + 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().awaitUninterruptibly(); + cb.shutdown(); + sb.shutdown(); + cb.releaseExternalResources(); + sb.releaseExternalResources(); + } + } + } + + private void sendAndWaitForFrame(Channel cc, SpdyFrame frame, CaptureHandler handler) { + cc.write(frame); + long theFuture = System.currentTimeMillis() + 3000; + while (handler.message == null && System.currentTimeMillis() < theFuture) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Ignore. + } + } + } + + private void addHeader(SpdyHeadersFrame frame, int headerNameSize, int headerValueSize) { + frame.addHeader("k", "v"); + StringBuilder headerName = new StringBuilder(); + for (int i = 0; i < headerNameSize; i++) { + headerName.append('h'); + } + StringBuilder headerValue = new StringBuilder(); + for (int i = 0; i < headerValueSize; i++) { + headerValue.append('a'); + } + frame.addHeader(headerName.toString(), headerValue.toString()); + } + + protected ChannelFactory newClientSocketChannelFactory(Executor executor) { + return new NioClientSocketChannelFactory(executor, executor); + } + + protected ChannelFactory newServerSocketChannelFactory(Executor executor) { + return new NioServerSocketChannelFactory(executor, executor); + } + + private static class CaptureHandler extends SimpleChannelUpstreamHandler { + public volatile Object message; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + this.message = e.getMessage(); + } + + @Override + public void exceptionCaught( + ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + System.out.println(e.getCause()); + e.getCause().printStackTrace(); + message = e.getCause(); + } + } +}