HTTPCORE-606: HTTP/2 protocol handler incorrectly uses larger frame size prior to SETTING handshake completion; revision of SETTINGS handshake implementation
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java
index 6fdbd35..27f8a34 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java
@@ -40,7 +40,8 @@
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class H2Config {
- public static final H2Config DEFAULT = new Builder().build();
+ public static final H2Config DEFAULT = custom().build();
+ public static final H2Config INIT = initial().build();
private final int headerTableSize;
private final boolean pushEnabled;
@@ -101,6 +102,21 @@
return new Builder();
}
+ private static final int INIT_HEADER_TABLE_SIZE = 4096;
+ private static final boolean INIT_ENABLE_PUSH = true;
+ private static final int INIT_MAX_FRAME_SIZE = FrameConsts.MIN_FRAME_SIZE;
+ private static final int INIT_WINDOW_SIZE = 65535;
+
+ public static H2Config.Builder initial() {
+ return new Builder()
+ .setHeaderTableSize(INIT_HEADER_TABLE_SIZE)
+ .setPushEnabled(INIT_ENABLE_PUSH)
+ .setMaxConcurrentStreams(Integer.MAX_VALUE) // no limit
+ .setMaxFrameSize(INIT_MAX_FRAME_SIZE)
+ .setInitialWindowSize(INIT_WINDOW_SIZE)
+ .setHeaderTableSize(Integer.MAX_VALUE); // unlimited
+ }
+
public static H2Config.Builder copy(final H2Config config) {
Args.notNull(config, "Connection config");
return new Builder()
@@ -122,10 +138,10 @@
private int maxHeaderListSize;
Builder() {
- this.headerTableSize = 8192;
- this.pushEnabled = false;
- this.maxConcurrentStreams = 100;
- this.initialWindowSize = 65535;
+ this.headerTableSize = INIT_HEADER_TABLE_SIZE * 2;
+ this.pushEnabled = INIT_ENABLE_PUSH;
+ this.maxConcurrentStreams = 250;
+ this.initialWindowSize = INIT_WINDOW_SIZE;
this.maxFrameSize = FrameConsts.MIN_FRAME_SIZE * 4;
this.maxHeaderListSize = FrameConsts.MAX_FRAME_SIZE;
}
@@ -170,7 +186,7 @@
headerTableSize,
pushEnabled,
maxConcurrentStreams,
- initialWindowSize > 0 ? initialWindowSize : 65535,
+ initialWindowSize,
maxFrameSize,
maxHeaderListSize);
}
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
index 9ebfaf4..7e6bb0b 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
@@ -122,7 +122,12 @@
private ConnectionHandshake connState = ConnectionHandshake.READY;
private SettingsHandshake localSettingState = SettingsHandshake.READY;
private SettingsHandshake remoteSettingState = SettingsHandshake.READY;
- private H2Config remoteConfig;
+
+ private int initInputWinSize;
+ private int initOutputWinSize;
+
+ private volatile H2Config remoteConfig;
+
private int lowMark;
private Continuation continuation;
@@ -145,7 +150,7 @@
this.localConfig = h2Config != null ? h2Config : H2Config.DEFAULT;
this.inputMetrics = new BasicH2TransportMetrics();
this.outputMetrics = new BasicH2TransportMetrics();
- this.connMetrics = new BasicHttpConnectionMetrics(inputMetrics, outputMetrics);
+ this.connMetrics = new BasicHttpConnectionMetrics(this.inputMetrics, this.outputMetrics);
this.inputBuffer = new FrameInputBuffer(this.inputMetrics, this.localConfig.getMaxFrameSize());
this.outputBuffer = new FrameOutputBuffer(this.outputMetrics, this.localConfig.getMaxFrameSize());
this.outputQueue = new ConcurrentLinkedDeque<>();
@@ -155,14 +160,17 @@
this.hPackEncoder = new HPackEncoder(CharCodingSupport.createEncoder(charCodingConfig));
this.hPackDecoder = new HPackDecoder(CharCodingSupport.createDecoder(charCodingConfig));
this.streamMap = new ConcurrentHashMap<>();
- this.connInputWindow = new AtomicInteger(localConfig.getInitialWindowSize());
- this.connOutputWindow = new AtomicInteger(H2Config.DEFAULT.getInitialWindowSize());
+ this.remoteConfig = H2Config.INIT;
+ this.connInputWindow = new AtomicInteger(H2Config.INIT.getInitialWindowSize());
+ this.connOutputWindow = new AtomicInteger(H2Config.INIT.getInitialWindowSize());
- this.hPackDecoder.setMaxTableSize(H2Config.DEFAULT.getInitialWindowSize());
- this.hPackEncoder.setMaxTableSize(this.localConfig.getHeaderTableSize());
+ this.initInputWinSize = H2Config.INIT.getInitialWindowSize();
+ this.initOutputWinSize = H2Config.INIT.getInitialWindowSize();
- this.remoteConfig = H2Config.DEFAULT;
- this.lowMark = this.remoteConfig.getInitialWindowSize() / 2;
+ this.hPackDecoder.setMaxTableSize(H2Config.INIT.getHeaderTableSize());
+ this.hPackEncoder.setMaxTableSize(H2Config.INIT.getHeaderTableSize());
+
+ this.lowMark = H2Config.INIT.getInitialWindowSize() / 2;
this.streamListener = streamListener;
}
@@ -341,8 +349,7 @@
if (capacity <= 0) {
return 0;
}
- final int frameSize = Math.max(localConfig.getMaxFrameSize(), remoteConfig.getMaxFrameSize());
- final int maxPayloadSize = Math.min(capacity, frameSize);
+ final int maxPayloadSize = Math.min(capacity, remoteConfig.getMaxFrameSize());
final int chunk;
if (payload.remaining() <= maxPayloadSize) {
chunk = payload.remaining();
@@ -461,7 +468,7 @@
final int connWinSize = connInputWindow.get();
if (connWinSize < lowMark) {
- final int delta = this.remoteConfig.getInitialWindowSize() - connWinSize;
+ final int delta = initInputWinSize - connWinSize;
if (delta > 0) {
final RawFrame windowUpdateFrame = frameFactory.createWindowUpdate(0, delta);
commitFrame(windowUpdateFrame);
@@ -610,11 +617,7 @@
} else if (command instanceof ExecutableCommand) {
final int streamId = generateStreamId();
final H2StreamChannelImpl channel = new H2StreamChannelImpl(
- streamId,
- true,
- localConfig.getInitialWindowSize(),
- remoteConfig.getInitialWindowSize());
-
+ streamId, true, initInputWinSize, initOutputWinSize);
final ExecutableCommand executableCommand = (ExecutableCommand) command;
final H2StreamHandler streamHandler = createLocallyInitiatedStream(
executableCommand, channel, httpProcessor, connMetrics);
@@ -757,10 +760,7 @@
updateLastStreamId(streamId);
final H2StreamChannelImpl channel = new H2StreamChannelImpl(
- streamId,
- false,
- localConfig.getInitialWindowSize(),
- remoteConfig.getInitialWindowSize());
+ streamId, false, initInputWinSize, initOutputWinSize);
final H2StreamHandler streamHandler = createRemotelyInitiatedStream(
channel, httpProcessor, connMetrics, null);
stream = new H2Stream(channel, streamHandler, true);
@@ -891,6 +891,7 @@
if (localSettingState == SettingsHandshake.TRANSMITTED) {
localSettingState = SettingsHandshake.ACKED;
ioSession.setEvent(SelectionKey.OP_WRITE);
+ applyLocalSettings();
}
} else {
final ByteBuffer payload = frame.getPayload();
@@ -939,10 +940,7 @@
updateLastStreamId(promisedStreamId);
final H2StreamChannelImpl channel = new H2StreamChannelImpl(
- promisedStreamId,
- false,
- localConfig.getInitialWindowSize(),
- remoteConfig.getInitialWindowSize());
+ promisedStreamId, false, initInputWinSize, initOutputWinSize);
final H2StreamHandler streamHandler = createRemotelyInitiatedStream(
channel, httpProcessor, connMetrics, stream.getPushHandlerFactory());
final H2Stream promisedStream = new H2Stream(channel, streamHandler, true);
@@ -1119,7 +1117,7 @@
}
private void consumeSettingsFrame(final ByteBuffer payload) throws HttpException, IOException {
- final H2Config.Builder configBuilder = H2Config.custom();
+ final H2Config.Builder configBuilder = H2Config.initial();
while (payload.hasRemaining()) {
final int code = payload.getShort();
final H2Param param = H2Param.valueOf(code);
@@ -1127,8 +1125,11 @@
final int value = payload.getInt();
switch (param) {
case HEADER_TABLE_SIZE:
- configBuilder.setHeaderTableSize(value);
- hPackDecoder.setMaxTableSize(value);
+ try {
+ configBuilder.setHeaderTableSize(value);
+ } catch (final IllegalArgumentException ex) {
+ throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
+ }
break;
case MAX_CONCURRENT_STREAMS:
try {
@@ -1146,21 +1147,6 @@
} catch (final IllegalArgumentException ex) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
}
- final int delta = value - remoteConfig.getInitialWindowSize();
- if (delta != 0) {
- updateOutputWindow(0, connOutputWindow, delta);
- if (!streamMap.isEmpty()) {
- for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
- final Map.Entry<Integer, H2Stream> entry = it.next();
- final H2Stream stream = entry.getValue();
- try {
- updateOutputWindow(stream.getId(), stream.getOutputWindow(), delta);
- } catch (final ArithmeticException ex) {
- throw new H2ConnectionException(H2Error.FLOW_CONTROL_ERROR, ex.getMessage());
- }
- }
- }
- }
break;
case MAX_FRAME_SIZE:
try {
@@ -1179,8 +1165,7 @@
}
}
}
- remoteConfig = configBuilder.build();
- lowMark = remoteConfig.getInitialWindowSize() / 2;
+ applyRemoteSettings(configBuilder.build());
}
private void produceOutput() throws HttpException, IOException {
@@ -1200,6 +1185,52 @@
}
}
+ private void applyRemoteSettings(final H2Config config) throws H2ConnectionException {
+ remoteConfig = config;
+
+ hPackDecoder.setMaxTableSize(remoteConfig.getMaxHeaderListSize());
+ final int delta = remoteConfig.getInitialWindowSize() - initOutputWinSize;
+ initOutputWinSize = remoteConfig.getInitialWindowSize();
+
+ if (delta != 0) {
+ updateOutputWindow(0, connOutputWindow, delta);
+ if (!streamMap.isEmpty()) {
+ for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry<Integer, H2Stream> entry = it.next();
+ final H2Stream stream = entry.getValue();
+ try {
+ updateOutputWindow(stream.getId(), stream.getOutputWindow(), delta);
+ } catch (final ArithmeticException ex) {
+ throw new H2ConnectionException(H2Error.FLOW_CONTROL_ERROR, ex.getMessage());
+ }
+ }
+ }
+ }
+ lowMark = initOutputWinSize / 2;
+ }
+
+ private void applyLocalSettings() throws H2ConnectionException {
+ hPackEncoder.setMaxTableSize(localConfig.getMaxHeaderListSize());
+
+ final int delta = localConfig.getInitialWindowSize() - initInputWinSize;
+ initInputWinSize = localConfig.getInitialWindowSize();
+
+ if (delta != 0) {
+ updateInputWindow(0, connInputWindow, delta);
+ if (!streamMap.isEmpty()) {
+ for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry<Integer, H2Stream> entry = it.next();
+ final H2Stream stream = entry.getValue();
+ try {
+ updateInputWindow(stream.getId(), stream.getInputWindow(), delta);
+ } catch (final ArithmeticException ex) {
+ throw new H2ConnectionException(H2Error.FLOW_CONTROL_ERROR, ex.getMessage());
+ }
+ }
+ }
+ }
+ }
+
@Override
public void close() throws IOException {
ioSession.enqueue(ShutdownCommand.GRACEFUL, Command.Priority.IMMEDIATE);
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java
index 87576a3..cb5e381 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java
@@ -65,7 +65,7 @@
Args.notNull(metrics, "HTTP2 transport metrcis");
Args.positive(maxFramePayloadSize, "Maximum payload size");
this.metrics = metrics;
- this.maxFramePayloadSize = maxFramePayloadSize;
+ this.maxFramePayloadSize = Math.max(maxFramePayloadSize, FrameConsts.MIN_FRAME_SIZE);
this.bytes = new byte[bufferLen];
this.buffer = ByteBuffer.wrap(bytes);
this.buffer.flip();