diff --git a/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift b/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift index 900ac976..8cae772f 100644 --- a/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift +++ b/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift @@ -67,6 +67,7 @@ struct HTTP2ConnectionStateMachine { let role: ConnectionRole var headerBlockValidation: ValidationState var contentLengthValidation: ValidationState + var maxResetStreams: Int } /// The state required for a connection that has sent a connection preface. @@ -98,7 +99,7 @@ struct HTTP2ConnectionStateMachine { self.headerBlockValidation = idleState.headerBlockValidation self.contentLengthValidation = idleState.contentLengthValidation self.localSettings = settings - self.streamState = ConnectionStreamState() + self.streamState = ConnectionStreamState(maxResetStreams: idleState.maxResetStreams) self.inboundFlowControlWindow = HTTP2FlowControlWindow(initialValue: settings.initialWindowSize) self.outboundFlowControlWindow = HTTP2FlowControlWindow( @@ -136,7 +137,7 @@ struct HTTP2ConnectionStateMachine { self.headerBlockValidation = idleState.headerBlockValidation self.contentLengthValidation = idleState.contentLengthValidation self.remoteSettings = settings - self.streamState = ConnectionStreamState() + self.streamState = ConnectionStreamState(maxResetStreams: idleState.maxResetStreams) self.inboundFlowControlWindow = HTTP2FlowControlWindow( initialValue: HTTP2SettingsState.defaultInitialWindowSize @@ -571,13 +572,15 @@ struct HTTP2ConnectionStateMachine { init( role: ConnectionRole, headerBlockValidation: ValidationState = .enabled, - contentLengthValidation: ValidationState = .enabled + contentLengthValidation: ValidationState = .enabled, + maxResetStreams: Int = 32 ) { self.state = .idle( .init( role: role, headerBlockValidation: headerBlockValidation, - contentLengthValidation: contentLengthValidation + contentLengthValidation: contentLengthValidation, + maxResetStreams: maxResetStreams ) ) } diff --git a/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift b/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift index 0815da86..3db5722f 100644 --- a/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift +++ b/Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift @@ -31,11 +31,6 @@ struct ConnectionStreamState { /// this case. private var recentlyResetStreams: CircularBuffer - /// The maximum number of reset streams we'll persist. - /// - /// TODO (cory): Make this configurable! - private let maxResetStreams: Int = 32 - /// The current number of streams that are active and that were initiated by the client. private var clientStreamCount: UInt32 = 0 @@ -63,9 +58,12 @@ struct ConnectionStreamState { Int(self.clientStreamCount) + Int(self.serverStreamCount) } - init() { + /// Creates a new `ConnectionStreamState`. + /// + /// - Parameter maxResetStreams: The maximum number of reset streams we'll persist. + init(maxResetStreams: Int) { self.activeStreams = StreamMap() - self.recentlyResetStreams = CircularBuffer(initialCapacity: self.maxResetStreams) + self.recentlyResetStreams = CircularBuffer(initialCapacity: maxResetStreams) } /// Create stream state for a remotely pushed stream. diff --git a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift index 1f6c797b..e3f00813 100644 --- a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift +++ b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift @@ -40,6 +40,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { /// The default value for the maximum number of sequential CONTINUATION frames. private static let defaultMaximumSequentialContinuationFrames: Int = 5 + /// The default number of recently reset streams to track. + private static let defaultMaximumRecentlyResetFrames: Int = 32 /// The event loop on which this handler will do work. @usableFromInline internal let _eventLoop: EventLoop? @@ -233,6 +235,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { maximumSequentialEmptyDataFrames: 1, maximumBufferedControlFrames: 10000, maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, + maximumRecentlyResetStreams: Self.defaultMaximumRecentlyResetFrames, maximumResetFrameCount: 200, resetFrameCounterWindow: .seconds(30), maximumStreamErrorCount: 200, @@ -269,6 +272,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: maximumBufferedControlFrames, maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, + maximumRecentlyResetStreams: Self.defaultMaximumRecentlyResetFrames, maximumResetFrameCount: 200, resetFrameCounterWindow: .seconds(30), maximumStreamErrorCount: 200, @@ -297,6 +301,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, + maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength, maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount, @@ -313,6 +318,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { maximumSequentialEmptyDataFrames: Int, maximumBufferedControlFrames: Int, maximumSequentialContinuationFrames: Int, + maximumRecentlyResetStreams: Int, maximumResetFrameCount: Int, resetFrameCounterWindow: TimeAmount, maximumStreamErrorCount: Int, @@ -322,7 +328,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { self.stateMachine = HTTP2ConnectionStateMachine( role: .init(mode), headerBlockValidation: .init(headerBlockValidation), - contentLengthValidation: .init(contentLengthValidation) + contentLengthValidation: .init(contentLengthValidation), + maxResetStreams: maximumRecentlyResetStreams ) self.mode = mode self.initialSettings = initialSettings @@ -371,6 +378,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { maximumBufferedControlFrames: Int = 10000, maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, tolerateImpossibleStateTransitionsInDebugMode: Bool = false, + maximumRecentlyResetStreams: Int = NIOHTTP2Handler.defaultMaximumRecentlyResetFrames, maximumResetFrameCount: Int = 200, resetFrameCounterWindow: TimeAmount = .seconds(30), maximumStreamErrorCount: Int = 200, @@ -379,7 +387,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { self.stateMachine = HTTP2ConnectionStateMachine( role: .init(mode), headerBlockValidation: .init(headerBlockValidation), - contentLengthValidation: .init(contentLengthValidation) + contentLengthValidation: .init(contentLengthValidation), + maxResetStreams: maximumRecentlyResetStreams ) self.mode = mode self._eventLoop = nil @@ -1320,6 +1329,7 @@ extension NIOHTTP2Handler { maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, + maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength, maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount, @@ -1351,6 +1361,7 @@ extension NIOHTTP2Handler { maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, + maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength, maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount, @@ -1374,6 +1385,7 @@ extension NIOHTTP2Handler { public var maximumSequentialEmptyDataFrames: Int = 1 public var maximumBufferedControlFrames: Int = 10000 public var maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames + public var maximumRecentlyResetStreams: Int = NIOHTTP2Handler.defaultMaximumRecentlyResetFrames public init() {} } diff --git a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift index 5713fae6..5e592488 100644 --- a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift +++ b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift @@ -7676,6 +7676,60 @@ class ConnectionStateMachineTests: XCTestCase { self.client.receiveHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false) ) } + + func testMaxRecentlyResetStreamFrames() { + // Internally a circular buffer is used to track recently reset streams which has its capacity + // reserved on init. This is rounded up to the next power of 2. However, in order to + // not grow beyond that size its effective capacity is less than the allocated capacity. + // + // Setting maxResetStreams to 64 means the effective capacity is 63. + let effectiveMaxResetStreams = 63 + // Create one more stream than that to check that going beyond the limit causes an error. + let streamsToCreate = 64 + + self.client = HTTP2ConnectionStateMachine(role: .client, maxResetStreams: effectiveMaxResetStreams) + self.exchangePreamble() + + let streamIDs = (0..