Make the header list size limit configurable on both client and server side.

This commit is contained in:
Xudong Ma 2015-10-28 15:03:34 -07:00
parent a4f319f0c3
commit 750b1977ca
7 changed files with 121 additions and 24 deletions

View File

@ -145,6 +145,11 @@ public final class GrpcUtil {
*/
public static final int DEFAULT_MAX_MESSAGE_SIZE = 100 * 1024 * 1024;
/**
* The default maximum size (in bytes) for inbound header/trailer.
*/
public static final int DEFAULT_MAX_HEADER_LIST_SIZE = 8192;
/**
* The set of valid status codes for client cancellation.
*/

View File

@ -71,6 +71,7 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
private SslContext sslContext;
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
/**
* Creates a new builder with the given server address. This factory method is primarily intended
@ -160,14 +161,14 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
* is {@link #DEFAULT_FLOW_CONTROL_WINDOW}).
*/
public final NettyChannelBuilder flowControlWindow(int flowControlWindow) {
Preconditions.checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
this.flowControlWindow = flowControlWindow;
return this;
}
/**
* Sets the maximum message size allowed to be received on the channel. If not called,
* defaults to {@link io.grpc.internal.GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE}.
* defaults to {@link GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE}.
*/
public final NettyChannelBuilder maxMessageSize(int maxMessageSize) {
checkArgument(maxMessageSize >= 0, "maxMessageSize must be >= 0");
@ -175,6 +176,16 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
return this;
}
/**
* Sets the maximum size of header list allowed to be received on the channel. If not called,
* defaults to {@link GrpcUtil#DEFAULT_MAX_HEADER_LIST_SIZE}.
*/
public final NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) {
checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be > 0");
this.maxHeaderListSize = maxHeaderListSize;
return this;
}
/**
* Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT} or
* {@code PLAINTEXT_UPGRADE}.
@ -192,7 +203,7 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
@Override
protected ClientTransportFactory buildTransportFactory() {
return new NettyTransportFactory(channelType, negotiationType, sslContext,
eventLoopGroup, flowControlWindow, maxMessageSize);
eventLoopGroup, flowControlWindow, maxMessageSize, maxHeaderListSize);
}
@Override
@ -246,18 +257,21 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
private final boolean usingSharedGroup;
private final int flowControlWindow;
private final int maxMessageSize;
private final int maxHeaderListSize;
private NettyTransportFactory(Class<? extends Channel> channelType,
NegotiationType negotiationType,
SslContext sslContext,
EventLoopGroup group,
int flowControlWindow,
int maxMessageSize) {
int maxMessageSize,
int maxHeaderListSize) {
this.channelType = channelType;
this.negotiationType = negotiationType;
this.sslContext = sslContext;
this.flowControlWindow = flowControlWindow;
this.maxMessageSize = maxMessageSize;
this.maxHeaderListSize = maxHeaderListSize;
usingSharedGroup = group == null;
if (usingSharedGroup) {
// The group was unspecified, using the shared group.
@ -272,7 +286,7 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
ProtocolNegotiator negotiator =
createProtocolNegotiator(authority, negotiationType, sslContext);
return new NettyClientTransport(serverAddress, channelType, group, negotiator,
flowControlWindow, maxMessageSize, authority);
flowControlWindow, maxMessageSize, maxHeaderListSize, authority);
}
@Override

View File

@ -54,11 +54,14 @@ import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2FrameReader;
import io.netty.handler.codec.http2.Http2FrameWriter;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2HeadersDecoder;
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
import io.netty.handler.logging.LogLevel;
@ -81,6 +84,7 @@ class NettyClientTransport implements ClientTransport {
private final AsciiString authority;
private final int flowControlWindow;
private final int maxMessageSize;
private final int maxHeaderListSize;
// We should not send on the channel until negotiation completes. This is a hard requirement
// by SslHandler but is appropriate for HTTP/1.1 Upgrade as well.
private Channel channel;
@ -94,13 +98,15 @@ class NettyClientTransport implements ClientTransport {
NettyClientTransport(SocketAddress address, Class<? extends Channel> channelType,
EventLoopGroup group, ProtocolNegotiator negotiator,
int flowControlWindow, int maxMessageSize, String authority) {
int flowControlWindow, int maxMessageSize, int maxHeaderListSize,
String authority) {
Preconditions.checkNotNull(negotiator, "negotiator");
this.address = Preconditions.checkNotNull(address, "address");
this.group = Preconditions.checkNotNull(group, "group");
this.channelType = Preconditions.checkNotNull(channelType, "channelType");
this.flowControlWindow = flowControlWindow;
this.maxMessageSize = maxMessageSize;
this.maxHeaderListSize = maxHeaderListSize;
this.authority = new AsciiString(authority);
handler = newHandler();
@ -244,7 +250,9 @@ class NettyClientTransport implements ClientTransport {
private NettyClientHandler newHandler() {
Http2Connection connection = new DefaultHttp2Connection(false);
Http2FrameReader frameReader = new DefaultHttp2FrameReader();
Http2HeadersDecoder headersDecoder =
new DefaultHttp2HeadersDecoder(maxHeaderListSize, Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE);
Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder);
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter();
Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.DEBUG, getClass());

View File

@ -76,12 +76,13 @@ public class NettyServer implements Server {
private Channel channel;
private final int flowControlWindow;
private final int maxMessageSize;
private final int maxHeaderListSize;
private final ReferenceCounted eventLoopReferenceCounter = new EventLoopReferenceCounter();
NettyServer(SocketAddress address, Class<? extends ServerChannel> channelType,
@Nullable EventLoopGroup bossGroup, @Nullable EventLoopGroup workerGroup,
@Nullable SslContext sslContext, int maxStreamsPerConnection, int flowControlWindow,
int maxMessageSize) {
int maxMessageSize, int maxHeaderListSize) {
this.address = address;
this.channelType = checkNotNull(channelType, "channelType");
this.bossGroup = bossGroup;
@ -92,6 +93,7 @@ public class NettyServer implements Server {
this.maxStreamsPerConnection = maxStreamsPerConnection;
this.flowControlWindow = flowControlWindow;
this.maxMessageSize = maxMessageSize;
this.maxHeaderListSize = maxHeaderListSize;
}
@Override
@ -119,7 +121,7 @@ public class NettyServer implements Server {
});
NettyServerTransport transport
= new NettyServerTransport(ch, sslContext, maxStreamsPerConnection, flowControlWindow,
maxMessageSize);
maxMessageSize, maxHeaderListSize);
transport.start(listener.transportCreated(transport));
}
});

View File

@ -39,6 +39,7 @@ import com.google.common.base.Preconditions;
import io.grpc.ExperimentalApi;
import io.grpc.HandlerRegistry;
import io.grpc.internal.AbstractServerImplBuilder;
import io.grpc.internal.GrpcUtil;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ServerChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@ -68,6 +69,7 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder<NettySer
private int maxConcurrentCallsPerConnection = Integer.MAX_VALUE;
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
/**
* Creates a server builder that will bind to the given port.
@ -182,7 +184,7 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder<NettySer
* limit.
*/
public NettyServerBuilder maxConcurrentCallsPerConnection(int maxCalls) {
Preconditions.checkArgument(maxCalls > 0, "max must be positive: %s", maxCalls);
checkArgument(maxCalls > 0, "max must be positive: %s", maxCalls);
this.maxConcurrentCallsPerConnection = maxCalls;
return this;
}
@ -192,7 +194,7 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder<NettySer
* is {@link #DEFAULT_FLOW_CONTROL_WINDOW}).
*/
public NettyServerBuilder flowControlWindow(int flowControlWindow) {
Preconditions.checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
this.flowControlWindow = flowControlWindow;
return this;
}
@ -207,11 +209,21 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder<NettySer
return this;
}
/**
* Sets the maximum size of header list allowed to be received on the server. If not called,
* defaults to {@link GrpcUtil#DEFAULT_MAX_HEADER_LIST_SIZE}.
*/
public NettyServerBuilder maxHeaderListSize(int maxHeaderListSize) {
checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be > 0");
this.maxHeaderListSize = maxHeaderListSize;
return this;
}
@Override
protected NettyServer buildTransportServer() {
return new NettyServer(address, channelType, bossEventLoopGroup,
workerEventLoopGroup, sslContext, maxConcurrentCallsPerConnection, flowControlWindow,
maxMessageSize);
maxMessageSize, maxHeaderListSize);
}
@Override

View File

@ -42,10 +42,13 @@ import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2FrameReader;
import io.netty.handler.codec.http2.Http2FrameWriter;
import io.netty.handler.codec.http2.Http2HeadersDecoder;
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
import io.netty.handler.logging.LogLevel;
@ -70,14 +73,16 @@ class NettyServerTransport implements ServerTransport {
private boolean terminated;
private final int flowControlWindow;
private final int maxMessageSize;
private final int maxHeaderListSize;
NettyServerTransport(Channel channel, @Nullable SslContext sslContext, int maxStreams,
int flowControlWindow, int maxMessageSize) {
int flowControlWindow, int maxMessageSize, int maxHeaderListSize) {
this.channel = Preconditions.checkNotNull(channel, "channel");
this.sslContext = sslContext;
this.maxStreams = maxStreams;
this.flowControlWindow = flowControlWindow;
this.maxMessageSize = maxMessageSize;
this.maxHeaderListSize = maxHeaderListSize;
}
public void start(ServerTransportListener listener) {
@ -133,8 +138,10 @@ class NettyServerTransport implements ServerTransport {
private NettyServerHandler createHandler(ServerTransportListener transportListener) {
Http2Connection connection = new DefaultHttp2Connection(true);
Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.DEBUG, getClass());
Http2FrameReader frameReader =
new Http2InboundFrameLogger(new DefaultHttp2FrameReader(), frameLogger);
Http2HeadersDecoder headersDecoder =
new DefaultHttp2HeadersDecoder(maxHeaderListSize, Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE);
Http2FrameReader frameReader = new Http2InboundFrameLogger(
new DefaultHttp2FrameReader(headersDecoder), frameLogger);
Http2FrameWriter frameWriter =
new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(), frameLogger);

View File

@ -61,6 +61,7 @@ import io.grpc.testing.TestUtils;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
@ -154,14 +155,15 @@ public class NettyClientTransportTest {
assertEquals(1, serverListener.streamListeners.size());
Metadata receivedHeaders = serverListener.streamListeners.get(0).headers;
assertEquals(GrpcUtil.getGrpcUserAgent("netty", userAgent),
receivedHeaders.get(USER_AGENT_KEY));
receivedHeaders.get(USER_AGENT_KEY));
}
@Test
public void maxMessageSizeShouldBeEnforced() throws Throwable {
startServer();
// Allow the response payloads of up to 1 byte.
NettyClientTransport transport = newTransport(newNegotiator(), 1);
NettyClientTransport transport = newTransport(newNegotiator(),
1, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE);
transport.start(clientTransportListener);
try {
@ -205,7 +207,7 @@ public class NettyClientTransportTest {
@Test
public void bufferedStreamsShouldBeClosedWhenConnectionTerminates() throws Exception {
// Only allow a single stream active at a time.
startServer(1);
startServer(1, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE);
NettyClientTransport transport = newTransport(newNegotiator());
transport.start(clientTransportListener);
@ -234,6 +236,51 @@ public class NettyClientTransportTest {
}
}
@Test
public void maxHeaderListSizeShouldBeEnforcedOnClient() throws Exception {
startServer();
NettyClientTransport transport = newTransport(newNegotiator(), DEFAULT_MAX_MESSAGE_SIZE, 1);
transport.start(clientTransportListener);
try {
// Send a single RPC and wait for the response.
new Rpc(transport, new Metadata()).halfClose().waitForResponse();
fail("The stream should have been failed due to client received header exceeds header list"
+ " size limit!");
} catch (Exception e) {
Throwable rootCause = getRootCause(e);
assertTrue(rootCause instanceof Http2Exception);
assertEquals("Header size exceeded max allowed size (1)", rootCause.getMessage());
}
}
@Test
public void maxHeaderListSizeShouldBeEnforcedOnServer() throws Exception {
startServer(100, 1);
NettyClientTransport transport = newTransport(newNegotiator());
transport.start(clientTransportListener);
try {
// Send a single RPC and wait for the response.
new Rpc(transport, new Metadata()).halfClose().waitForResponse();
fail("The stream should have been failed due to server received header exceeds header list"
+ " size limit!");
} catch (Exception e) {
Throwable rootCause = getRootCause(e);
assertTrue(rootCause.getMessage(),
rootCause.getMessage().contains("Header size exceeded max allowed size (1)"));
}
}
private Throwable getRootCause(Throwable t) {
if (t.getCause() == null) {
return t;
}
return getRootCause(t.getCause());
}
private ProtocolNegotiator newNegotiator() throws IOException {
File clientCert = TestUtils.loadCert("ca.pem");
SslContext clientContext = GrpcSslContexts.forClient().trustManager(clientCert)
@ -242,28 +289,30 @@ public class NettyClientTransportTest {
}
private NettyClientTransport newTransport(ProtocolNegotiator negotiator) {
return newTransport(negotiator, DEFAULT_MAX_MESSAGE_SIZE);
return newTransport(negotiator,
DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE);
}
private NettyClientTransport newTransport(ProtocolNegotiator negotiator, int maxMsgSize) {
private NettyClientTransport newTransport(ProtocolNegotiator negotiator,
int maxMsgSize, int maxHeaderListSize) {
NettyClientTransport transport = new NettyClientTransport(address, NioSocketChannel.class,
group, negotiator, DEFAULT_WINDOW_SIZE, maxMsgSize, authority);
group, negotiator, DEFAULT_WINDOW_SIZE, maxMsgSize, maxHeaderListSize, authority);
transports.add(transport);
return transport;
}
private void startServer() throws IOException {
startServer(100);
startServer(100, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE);
}
private void startServer(int maxStreamsPerConnection) throws IOException {
private void startServer(int maxStreamsPerConnection, int maxHeaderListSize) throws IOException {
File serverCert = TestUtils.loadCert("server1.pem");
File key = TestUtils.loadCert("server1.key");
SslContext serverContext = GrpcSslContexts.forServer(serverCert, key)
.ciphers(TestUtils.preferredTestCiphers(), SupportedCipherSuiteFilter.INSTANCE).build();
server = new NettyServer(address, NioServerSocketChannel.class,
group, group, serverContext, maxStreamsPerConnection,
DEFAULT_WINDOW_SIZE, DEFAULT_MAX_MESSAGE_SIZE);
DEFAULT_WINDOW_SIZE, DEFAULT_MAX_MESSAGE_SIZE, maxHeaderListSize);
server.start(serverListener);
}