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 96146ce91..00c68113f 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
@@ -104,6 +104,8 @@
abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection {
+ private final H2ControlFrameRateLimiter controlFrameRateLimiter;
+
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024;
enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN }
@@ -188,6 +190,21 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
final H2StreamListener streamListener,
final Timeout validateAfterInactivity,
final Timeout pingAckTimeout) {
+ this(ioSession, frameFactory, idGenerator, httpProcessor, charCodingConfig, h2Config, streamListener,
+ validateAfterInactivity, pingAckTimeout, new H2ControlFrameRateLimiter());
+ }
+
+ AbstractH2StreamMultiplexer(
+ final ProtocolIOSession ioSession,
+ final FrameFactory frameFactory,
+ final StreamIdGenerator idGenerator,
+ final HttpProcessor httpProcessor,
+ final CharCodingConfig charCodingConfig,
+ final H2Config h2Config,
+ final H2StreamListener streamListener,
+ final Timeout validateAfterInactivity,
+ final Timeout pingAckTimeout,
+ final H2ControlFrameRateLimiter controlFrameRateLimiter) {
this.ioSession = Args.notNull(ioSession, "IO session");
this.frameFactory = Args.notNull(frameFactory, "Frame factory");
this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor");
@@ -216,6 +233,7 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
this.lastActivityTime = System.currentTimeMillis();
this.validateAfterInactivity = validateAfterInactivity;
this.pingAckTimeout = Args.notNull(pingAckTimeout, "PING ACK timeout");
+ this.controlFrameRateLimiter = Args.notNull(controlFrameRateLimiter, "Control frame rate limiter");
}
@Override
@@ -976,7 +994,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
pingHandler.consumeResponse(ping);
}
} else {
- final ByteBuffer pong = ByteBuffer.allocate(ping.remaining());
+ if (!controlFrameRateLimiter.accept(System.nanoTime())) {
+ throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive HTTP/2 PING / SETTINGS frames");
+ }
+ final ByteBuffer pong = ByteBuffer.allocate(ping.remaining());
pong.put(ping);
pong.flip();
final RawFrame response = frameFactory.createPingAck(pong);
@@ -1005,10 +1026,15 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
}
} else {
final ByteBuffer payload = frame.getPayload();
+ if (payload != null && (payload.remaining() % 6) != 0) {
+ throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR,
+ "Invalid SETTINGS payload");
+ }
+ if (!controlFrameRateLimiter.accept(System.nanoTime())) {
+ throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM,
+ "Excessive HTTP/2 PING / SETTINGS frames");
+ }
if (payload != null) {
- if ((payload.remaining() % 6) != 0) {
- throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid SETTINGS payload");
- }
consumeSettingsFrame(payload);
remoteSettingState = SettingsHandshake.TRANSMITTED;
}
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java
new file mode 100644
index 000000000..77a696ecd
--- /dev/null
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ *
The limiter is owned and called by a single HTTP/2 I/O session. Only + * non-ACK PING and SETTINGS frames are charged. ACK frames, DATA, + * WINDOW_UPDATE and RST_STREAM are not charged, as they can represent + * legitimate high-throughput or highly concurrent exchanges.
+ * + *Uses a small token bucket to tolerate bursts and recover over time, + * with no locks or per-frame allocation. This is a resource safeguard, + * not an RFC 9113 protocol-validity rule.
+ */ +final class H2ControlFrameRateLimiter { + + static final int CAPACITY = 256; + static final int REFILL_PER_SECOND = 128; + private static final long NANOS_PER_SECOND = 1_000_000_000L; + + private final int capacity; + private final int refillPerSecond; + private final long fullRefillNanos; + + private int tokens; + private long previousNanos; + private long remainder; + private boolean initialized; + + H2ControlFrameRateLimiter(final int capacity, final int refillPerSecond) { + this.capacity = Args.positive(capacity, "Capacity"); + this.refillPerSecond = Args.positive(refillPerSecond, "Refill per second"); + this.fullRefillNanos = (NANOS_PER_SECOND * capacity + refillPerSecond - 1) / refillPerSecond; + this.tokens = capacity; + } + + H2ControlFrameRateLimiter() { + this(CAPACITY, REFILL_PER_SECOND); + } + + boolean accept(final long nowNanos) { + if (!initialized) { + initialized = true; + previousNanos = nowNanos; + } else { + final long elapsed = nowNanos - previousNanos; + // Avoid overflow and do not refill on a backward clock jump. + if (elapsed > 0) { + previousNanos = nowNanos; + if (elapsed >= fullRefillNanos) { + tokens = capacity; + remainder = 0; + } else { + final long numerator = elapsed * refillPerSecond + remainder; + final long refill = numerator / NANOS_PER_SECOND; + remainder = numerator % NANOS_PER_SECOND; + if (refill > 0) { + tokens = (int) Math.min(capacity, (long) tokens + refill); + if (tokens == capacity) { + remainder = 0; + } + } + } + } + } + if (tokens == 0) { + return false; + } + tokens--; + return true; + } +} diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java index 5ec17cd97..54788d68a 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java @@ -156,6 +156,23 @@ public H2StreamMultiplexerImpl( this.streamHandlerSupplier = streamHandlerSupplier; } + public H2StreamMultiplexerImpl( + final ProtocolIOSession ioSession, + final FrameFactory frameFactory, + final StreamIdGenerator idGenerator, + final HttpProcessor httpProcessor, + final CharCodingConfig charCodingConfig, + final H2Config h2Config, + final H2StreamListener streamListener, + final Supplier