SPDY: handle too large header blocks
- Forward-ported 22b8a96e044b77e5fadc5a1217080a1f9c69aa9c
This commit is contained in:
parent
c77f03d886
commit
e0805ecea9
@ -232,6 +232,12 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -15,10 +15,10 @@
|
||||
*/
|
||||
package io.netty.handler.codec.spdy;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.netty.util.internal.StringUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The default {@link SpdyHeadersFrame} implementation.
|
||||
*/
|
||||
@ -26,6 +26,7 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame
|
||||
implements SpdyHeadersFrame {
|
||||
|
||||
private boolean invalid;
|
||||
private boolean truncated;
|
||||
private final SpdyHeaders headers = new DefaultSpdyHeaders();
|
||||
|
||||
/**
|
||||
@ -49,15 +50,27 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
return invalid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpdyHeadersFrame setInvalid() {
|
||||
invalid = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isTruncated() {
|
||||
return truncated;
|
||||
}
|
||||
|
||||
public SpdyHeadersFrame setTruncated() {
|
||||
truncated = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpdyHeaders headers() {
|
||||
return headers;
|
||||
}
|
||||
|
@ -241,7 +241,9 @@ public class SpdyFrameDecoder extends ByteToMessageDecoder {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spdyHeadersFrame != null && spdyHeadersFrame.isInvalid()) {
|
||||
if (spdyHeadersFrame != null &&
|
||||
(spdyHeadersFrame.isInvalid() || spdyHeadersFrame.isTruncated())) {
|
||||
|
||||
Object frame = spdyHeadersFrame;
|
||||
spdyHeadersFrame = null;
|
||||
if (length == 0) {
|
||||
|
@ -16,7 +16,6 @@
|
||||
package io.netty.handler.codec.spdy;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.TooLongFrameException;
|
||||
|
||||
import static io.netty.handler.codec.spdy.SpdyCodecUtil.*;
|
||||
|
||||
@ -94,8 +93,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 +142,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
|
||||
|
@ -31,6 +31,17 @@ public interface SpdyHeadersFrame extends SpdyStreamFrame {
|
||||
*/
|
||||
SpdyHeadersFrame setInvalid();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this header block has been truncated due to
|
||||
* length restrictions.
|
||||
*/
|
||||
boolean isTruncated();
|
||||
|
||||
/**
|
||||
* Mark this header block as truncated.
|
||||
*/
|
||||
SpdyHeadersFrame setTruncated();
|
||||
|
||||
/**
|
||||
* Returns the {@link SpdyHeaders}.
|
||||
*/
|
||||
|
@ -108,7 +108,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
if (associatedToStreamId == 0) {
|
||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM);
|
||||
ctx.write(spdyRstStreamFrame);
|
||||
out.add(spdyRstStreamFrame);
|
||||
}
|
||||
|
||||
String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
|
||||
@ -118,7 +118,15 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
if (URL == null) {
|
||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
||||
ctx.write(spdyRstStreamFrame);
|
||||
out.add(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);
|
||||
out.add(spdyRstStreamFrame);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -141,10 +149,23 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
} catch (Exception e) {
|
||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
||||
ctx.write(spdyRstStreamFrame);
|
||||
out.add(spdyRstStreamFrame);
|
||||
}
|
||||
} 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);
|
||||
out.add(spdySynReplyFrame);
|
||||
}
|
||||
|
||||
try {
|
||||
FullHttpRequest httpRequestWithEntity = createHttpRequest(spdyVersion, spdySynStreamFrame);
|
||||
|
||||
@ -165,7 +186,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
spdySynReplyFrame.setLast(true);
|
||||
SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
|
||||
SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
|
||||
ctx.write(spdySynReplyFrame);
|
||||
out.add(spdySynReplyFrame);
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,6 +195,14 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
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);
|
||||
out.add(spdyRstStreamFrame);
|
||||
}
|
||||
|
||||
try {
|
||||
FullHttpResponse httpResponseWithEntity = createHttpResponse(spdyVersion, spdySynReplyFrame);
|
||||
|
||||
@ -192,7 +221,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
// the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
|
||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
||||
ctx.write(spdyRstStreamFrame);
|
||||
out.add(spdyRstStreamFrame);
|
||||
}
|
||||
|
||||
} else if (msg instanceof SpdyHeadersFrame) {
|
||||
@ -206,9 +235,12 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore trailers in a truncated HEADERS frame.
|
||||
if (!spdyHeadersFrame.isTruncated()) {
|
||||
for (Map.Entry<String, String> e: spdyHeadersFrame.headers().entries()) {
|
||||
fullHttpMessage.headers().add(e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (spdyHeadersFrame.isLast()) {
|
||||
HttpHeaders.setContentLength(fullHttpMessage, fullHttpMessage.content().readableBytes());
|
||||
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2013 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.bootstrap.Bootstrap;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
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 org.junit.Test;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static io.netty.handler.codec.spdy.SpdyConstants.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SpdyFrameDecoderTest {
|
||||
|
||||
private static final EventLoopGroup group = new NioEventLoopGroup();
|
||||
|
||||
@AfterClass
|
||||
public static void destroy() throws Exception {
|
||||
group.shutdownGracefully().sync();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooLargeHeaderNameOnSynStreamRequest() throws Exception {
|
||||
for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version++) {
|
||||
final int finalVersion = version;
|
||||
List<Integer> 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<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(
|
||||
new SpdyFrameDecoder(finalVersion, 10000, maxHeaderSize),
|
||||
new SpdySessionHandler(finalVersion, true),
|
||||
captureHandler);
|
||||
}
|
||||
});
|
||||
|
||||
Bootstrap cb = new Bootstrap();
|
||||
cb.group(group);
|
||||
cb.channel(NioSocketChannel.class);
|
||||
cb.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new SpdyFrameEncoder(finalVersion));
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static 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 static void addHeader(SpdyHeadersFrame frame, int headerNameSize, int headerValueSize) {
|
||||
frame.headers().add("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.headers().add(headerName.toString(), headerValue.toString());
|
||||
}
|
||||
|
||||
private static class CaptureHandler extends SimpleChannelInboundHandler<Object> {
|
||||
public volatile Object message;
|
||||
|
||||
@Override
|
||||
public void messageReceived(ChannelHandlerContext ctx, Object m) throws Exception {
|
||||
message = m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
cause.printStackTrace();
|
||||
message = cause;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user