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");
|
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
|
* 500 Internal Server Error
|
||||||
*/
|
*/
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.spdy;
|
package io.netty.handler.codec.spdy;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default {@link SpdyHeadersFrame} implementation.
|
* The default {@link SpdyHeadersFrame} implementation.
|
||||||
*/
|
*/
|
||||||
@ -26,6 +26,7 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame
|
|||||||
implements SpdyHeadersFrame {
|
implements SpdyHeadersFrame {
|
||||||
|
|
||||||
private boolean invalid;
|
private boolean invalid;
|
||||||
|
private boolean truncated;
|
||||||
private final SpdyHeaders headers = new DefaultSpdyHeaders();
|
private final SpdyHeaders headers = new DefaultSpdyHeaders();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,15 +50,27 @@ public class DefaultSpdyHeadersFrame extends DefaultSpdyStreamFrame
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isInvalid() {
|
public boolean isInvalid() {
|
||||||
return invalid;
|
return invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public SpdyHeadersFrame setInvalid() {
|
public SpdyHeadersFrame setInvalid() {
|
||||||
invalid = true;
|
invalid = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTruncated() {
|
||||||
|
return truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpdyHeadersFrame setTruncated() {
|
||||||
|
truncated = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public SpdyHeaders headers() {
|
public SpdyHeaders headers() {
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,9 @@ public class SpdyFrameDecoder extends ByteToMessageDecoder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spdyHeadersFrame != null && spdyHeadersFrame.isInvalid()) {
|
if (spdyHeadersFrame != null &&
|
||||||
|
(spdyHeadersFrame.isInvalid() || spdyHeadersFrame.isTruncated())) {
|
||||||
|
|
||||||
Object frame = spdyHeadersFrame;
|
Object frame = spdyHeadersFrame;
|
||||||
spdyHeadersFrame = null;
|
spdyHeadersFrame = null;
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package io.netty.handler.codec.spdy;
|
package io.netty.handler.codec.spdy;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.handler.codec.TooLongFrameException;
|
|
||||||
|
|
||||||
import static io.netty.handler.codec.spdy.SpdyCodecUtil.*;
|
import static io.netty.handler.codec.spdy.SpdyCodecUtil.*;
|
||||||
|
|
||||||
@ -94,8 +93,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder {
|
|||||||
}
|
}
|
||||||
headerSize += nameLength;
|
headerSize += nameLength;
|
||||||
if (headerSize > maxHeaderSize) {
|
if (headerSize > maxHeaderSize) {
|
||||||
throw new TooLongFrameException(
|
frame.setTruncated();
|
||||||
"Header block exceeds " + maxHeaderSize);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read name
|
// Try to read name
|
||||||
@ -143,8 +142,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder {
|
|||||||
|
|
||||||
headerSize += valueLength;
|
headerSize += valueLength;
|
||||||
if (headerSize > maxHeaderSize) {
|
if (headerSize > maxHeaderSize) {
|
||||||
throw new TooLongFrameException(
|
frame.setTruncated();
|
||||||
"Header block exceeds " + maxHeaderSize);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read value
|
// Try to read value
|
||||||
|
@ -31,6 +31,17 @@ public interface SpdyHeadersFrame extends SpdyStreamFrame {
|
|||||||
*/
|
*/
|
||||||
SpdyHeadersFrame setInvalid();
|
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}.
|
* Returns the {@link SpdyHeaders}.
|
||||||
*/
|
*/
|
||||||
|
@ -108,7 +108,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
|||||||
if (associatedToStreamId == 0) {
|
if (associatedToStreamId == 0) {
|
||||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM);
|
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM);
|
||||||
ctx.write(spdyRstStreamFrame);
|
out.add(spdyRstStreamFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
|
String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
|
||||||
@ -118,7 +118,15 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
|||||||
if (URL == null) {
|
if (URL == null) {
|
||||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
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 {
|
try {
|
||||||
@ -141,10 +149,23 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
||||||
ctx.write(spdyRstStreamFrame);
|
out.add(spdyRstStreamFrame);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SYN_STREAM frames initiated by the client are HTTP requests
|
// 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 {
|
try {
|
||||||
FullHttpRequest httpRequestWithEntity = createHttpRequest(spdyVersion, spdySynStreamFrame);
|
FullHttpRequest httpRequestWithEntity = createHttpRequest(spdyVersion, spdySynStreamFrame);
|
||||||
|
|
||||||
@ -165,7 +186,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
|||||||
spdySynReplyFrame.setLast(true);
|
spdySynReplyFrame.setLast(true);
|
||||||
SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
|
SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
|
||||||
SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
|
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;
|
SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
|
||||||
int streamId = spdySynReplyFrame.getStreamId();
|
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 {
|
try {
|
||||||
FullHttpResponse httpResponseWithEntity = createHttpResponse(spdyVersion, spdySynReplyFrame);
|
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
|
// the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
|
||||||
SpdyRstStreamFrame spdyRstStreamFrame =
|
SpdyRstStreamFrame spdyRstStreamFrame =
|
||||||
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
|
||||||
ctx.write(spdyRstStreamFrame);
|
out.add(spdyRstStreamFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (msg instanceof SpdyHeadersFrame) {
|
} else if (msg instanceof SpdyHeadersFrame) {
|
||||||
@ -206,9 +235,12 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore trailers in a truncated HEADERS frame.
|
||||||
|
if (!spdyHeadersFrame.isTruncated()) {
|
||||||
for (Map.Entry<String, String> e: spdyHeadersFrame.headers().entries()) {
|
for (Map.Entry<String, String> e: spdyHeadersFrame.headers().entries()) {
|
||||||
fullHttpMessage.headers().add(e.getKey(), e.getValue());
|
fullHttpMessage.headers().add(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (spdyHeadersFrame.isLast()) {
|
if (spdyHeadersFrame.isLast()) {
|
||||||
HttpHeaders.setContentLength(fullHttpMessage, fullHttpMessage.content().readableBytes());
|
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