diff --git a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift index fab1ae33..6908e457 100644 --- a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift +++ b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift @@ -39,6 +39,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { /// The magic string sent by clients at the start of a HTTP/2 connection. private static let clientMagic: StaticString = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + /// The default value for the maximum number of sequential CONTINUATION frames. + private static let defaultMaximumSequentialContinuationFrames: Int = 5 + /// The event loop on which this handler will do work. @usableFromInline internal let _eventLoop: EventLoop? @@ -104,6 +107,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { /// The delegate for (de)multiplexing inbound streams. private var inboundStreamMultiplexerState: InboundStreamMultiplexerState + /// The maximum number of sequential CONTINUATION frames. + private let maximumSequentialContinuationFrames: Int + @usableFromInline internal var inboundStreamMultiplexer: InboundStreamMultiplexer? { return self.inboundStreamMultiplexerState.multiplexer @@ -212,6 +218,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { contentLengthValidation: contentLengthValidation, maximumSequentialEmptyDataFrames: 1, maximumBufferedControlFrames: 10000, + maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, maximumResetFrameCount: 200, resetFrameCounterWindow: .seconds(30)) } @@ -241,6 +248,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { contentLengthValidation: contentLengthValidation, maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: maximumBufferedControlFrames, + maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, maximumResetFrameCount: 200, resetFrameCounterWindow: .seconds(30)) @@ -262,6 +270,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { contentLengthValidation: connectionConfiguration.contentLengthValidation, maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, + maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength) } @@ -273,6 +282,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { contentLengthValidation: ValidationState, maximumSequentialEmptyDataFrames: Int, maximumBufferedControlFrames: Int, + maximumSequentialContinuationFrames: Int, maximumResetFrameCount: Int, resetFrameCounterWindow: TimeAmount) { self._eventLoop = eventLoop @@ -283,6 +293,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { self.denialOfServiceValidator = DOSHeuristics(maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumResetFrameCount: maximumResetFrameCount, resetFrameCounterWindow: resetFrameCounterWindow) self.tolerateImpossibleStateTransitionsInDebugMode = false self.inboundStreamMultiplexerState = .uninitializedLegacy + self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames } /// Constructs a ``NIOHTTP2Handler``. @@ -297,6 +308,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { /// - maximumBufferedControlFrames: Controls the maximum buffer size of buffered outbound control frames. If we are unable to send control frames as /// fast as we produce them we risk building up an unbounded buffer and exhausting our memory. To protect against this DoS vector, we put an /// upper limit on the depth of this queue. Defaults to 10,000. + /// - maximumSequentialContinuationFrames: The maximum number of sequential CONTINUATION frames. /// - tolerateImpossibleStateTransitionsInDebugMode: Whether impossible state transitions should be tolerated /// in debug mode. /// - maximumResetFrameCount: Controls the maximum permitted reset frames within a given time window. Too many may exhaust CPU resources. To protect @@ -309,6 +321,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { contentLengthValidation: ValidationState = .enabled, maximumSequentialEmptyDataFrames: Int = 1, maximumBufferedControlFrames: Int = 10000, + maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames, tolerateImpossibleStateTransitionsInDebugMode: Bool = false, maximumResetFrameCount: Int = 200, resetFrameCounterWindow: TimeAmount = .seconds(30)) { @@ -320,10 +333,15 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { self.denialOfServiceValidator = DOSHeuristics(maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumResetFrameCount: maximumResetFrameCount, resetFrameCounterWindow: resetFrameCounterWindow) self.tolerateImpossibleStateTransitionsInDebugMode = tolerateImpossibleStateTransitionsInDebugMode self.inboundStreamMultiplexerState = .uninitializedLegacy + self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames } public func handlerAdded(context: ChannelHandlerContext) { - self.frameDecoder = HTTP2FrameDecoder(allocator: context.channel.allocator, expectClientMagic: self.mode == .server) + self.frameDecoder = HTTP2FrameDecoder( + allocator: context.channel.allocator, + expectClientMagic: self.mode == .server, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) self.frameEncoder = HTTP2FrameEncoder(allocator: context.channel.allocator) self.writeBuffer = context.channel.allocator.buffer(capacity: 128) self.inboundStreamMultiplexerState.initialize(context: context, http2Handler: self, mode: self.mode) @@ -495,6 +513,9 @@ extension NIOHTTP2Handler { } catch is NIOHTTP2Errors.ExcessivelyLargeHeaderBlock { self.inboundConnectionErrorTriggered(context: context, underlyingError: NIOHTTP2Errors.excessivelyLargeHeaderBlock(), reason: .protocolError) return nil + } catch is NIOHTTP2Errors.ExcessiveContinuationFrames { + self.inboundConnectionErrorTriggered(context: context, underlyingError: NIOHTTP2Errors.excessiveContinuationFrames(), reason: .enhanceYourCalm) + return nil } catch { self.inboundConnectionErrorTriggered(context: context, underlyingError: error, reason: .internalError) return nil @@ -1069,6 +1090,7 @@ extension NIOHTTP2Handler { contentLengthValidation: connectionConfiguration.contentLengthValidation, maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, + maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength ) @@ -1093,6 +1115,7 @@ extension NIOHTTP2Handler { contentLengthValidation: connectionConfiguration.contentLengthValidation, maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames, maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames, + maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames, maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount, resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength ) @@ -1109,6 +1132,7 @@ extension NIOHTTP2Handler { public var contentLengthValidation: ValidationState = .enabled public var maximumSequentialEmptyDataFrames: Int = 1 public var maximumBufferedControlFrames: Int = 10000 + public var maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames public init() {} } diff --git a/Sources/NIOHTTP2/HTTP2Error.swift b/Sources/NIOHTTP2/HTTP2Error.swift index 5bb0ab4b..e41d096b 100644 --- a/Sources/NIOHTTP2/HTTP2Error.swift +++ b/Sources/NIOHTTP2/HTTP2Error.swift @@ -298,6 +298,11 @@ public enum NIOHTTP2Errors { return ExcessiveRSTFrames(file: file, line: line) } + /// Creates an ``ExcessiveContinuationFrames`` error with appropriate source context. + public static func excessiveContinuationFrames(file: String = #fileID, line: UInt = #line) -> ExcessiveContinuationFrames { + return ExcessiveContinuationFrames(file: file, line: line) + } + /// Creates a ``StreamError`` error with appropriate source context. /// /// - Parameters: @@ -1754,6 +1759,26 @@ public enum NIOHTTP2Errors { return true } } + + /// A remote peer has sent a sequence of `CONTINUATION` frames longer than the configured limit. + public struct ExcessiveContinuationFrames: NIOHTTP2Error { + private let file: String + private let line: UInt + + /// The location where the error was thrown. + public var location: String { + return _location(file: self.file, line: self.line) + } + + fileprivate init(file: String, line: UInt) { + self.file = file + self.line = line + } + + public static func ==(lhs: Self, rhs: Self) -> Bool { + return true + } + } } diff --git a/Sources/NIOHTTP2/HTTP2FrameParser.swift b/Sources/NIOHTTP2/HTTP2FrameParser.swift index 3ba6066d..90f52f2c 100644 --- a/Sources/NIOHTTP2/HTTP2FrameParser.swift +++ b/Sources/NIOHTTP2/HTTP2FrameParser.swift @@ -277,7 +277,8 @@ struct HTTP2FrameDecoder { header: syntheticHeader, initialPayload: payloadBytes, incomingPayload: self.accumulatedBytes, - originalPaddingBytes: self.expectedPadding + originalPaddingBytes: self.expectedPadding, + continuationSequenceCount: 1 ) ) } @@ -504,6 +505,7 @@ struct HTTP2FrameDecoder { private var currentFrameBytes: ByteBuffer private var continuationPayload: ByteBuffer private var originalPaddingBytes: Int? + private var continuationSequenceCount: Int init(fromAccumulatingHeaderBlockFragments acc: AccumulatingHeaderBlockFragmentsParserState, continuationHeader: FrameHeader) { @@ -515,6 +517,7 @@ struct HTTP2FrameDecoder { self.currentFrameBytes = acc.accumulatedPayload self.continuationPayload = acc.incomingPayload self.originalPaddingBytes = acc.originalPaddingBytes + self.continuationSequenceCount = acc.continuationSequenceCount } /// The result of successful processing: we either produce a frame and move to the new accumulating state, @@ -556,7 +559,8 @@ struct HTTP2FrameDecoder { header: header, initialPayload: payload, incomingPayload: self.continuationPayload, - originalPaddingBytes: self.originalPaddingBytes + originalPaddingBytes: self.originalPaddingBytes, + continuationSequenceCount: self.continuationSequenceCount + 1 ) ) } @@ -582,16 +586,27 @@ struct HTTP2FrameDecoder { private(set) var accumulatedPayload: ByteBuffer private(set) var incomingPayload: ByteBuffer private(set) var originalPaddingBytes: Int? - - init(header: FrameHeader, initialPayload: ByteBuffer, incomingPayload: ByteBuffer, originalPaddingBytes: Int?) { + private(set) var continuationSequenceCount: Int + + init( + header: FrameHeader, + initialPayload: ByteBuffer, + incomingPayload: ByteBuffer, + originalPaddingBytes: Int?, + continuationSequenceCount: Int + ) { precondition(header.beginsContinuationSequence) self.header = header self.accumulatedPayload = initialPayload self.incomingPayload = incomingPayload self.originalPaddingBytes = originalPaddingBytes + self.continuationSequenceCount = continuationSequenceCount } - mutating func process(maxHeaderListSize: Int) throws -> AccumulatingContinuationPayloadParserState? { + mutating func process( + maxHeaderListSize: Int, + maximumSequentialContinuationFrames: Int + ) throws -> AccumulatingContinuationPayloadParserState? { // we have an entire HEADERS/PUSH_PROMISE frame, but one or more CONTINUATION frames // are arriving. Wait for them. guard let header = self.incomingPayload.readFrameHeader() else { @@ -614,6 +629,11 @@ struct HTTP2FrameDecoder { throw NIOHTTP2Errors.excessivelyLargeHeaderBlock() } + // The sequence of CONTINUATION frames received is not longer than the configured limit + guard self.continuationSequenceCount <= maximumSequentialContinuationFrames else { + throw NIOHTTP2Errors.excessiveContinuationFrames() + } + return AccumulatingContinuationPayloadParserState(fromAccumulatingHeaderBlockFragments: self, continuationHeader: header) } @@ -698,6 +718,7 @@ struct HTTP2FrameDecoder { internal var headerDecoder: HPACKDecoder private var state: ParserState + private let maximumSequentialContinuationFrames: Int // RFC 7540 ยง 6.5.2 puts the initial value of SETTINGS_MAX_FRAME_SIZE at 2**14 octets internal var maxFrameSize: UInt32 = 1<<14 @@ -708,8 +729,14 @@ struct HTTP2FrameDecoder { /// and decoding headers. /// - parameter expectClientMagic: Whether the parser should expect to receive the bytes of /// client magic string before frame parsing begins. - init(allocator: ByteBufferAllocator, expectClientMagic: Bool) { + /// - parameter maximumSequentialContinuationFrames: The maximum number of sequential CONTINUATION frames. + init( + allocator: ByteBufferAllocator, + expectClientMagic: Bool, + maximumSequentialContinuationFrames: Int + ) { self.headerDecoder = HPACKDecoder(allocator: allocator) + self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames if expectClientMagic { self.state = .awaitingClientMagic(ClientMagicState()) @@ -1001,9 +1028,14 @@ struct HTTP2FrameDecoder { case .accumulatingHeaderBlockFragments(var state): let maxHeaderListSize = self.headerDecoder.maxHeaderListSize + let maximumSequentialContinuationFrames = self.maximumSequentialContinuationFrames + return try self.avoidingParserCoW { newState in let result = Result { - guard let processResult = try state.process(maxHeaderListSize: maxHeaderListSize) else { + guard let processResult = try state.process( + maxHeaderListSize: maxHeaderListSize, + maximumSequentialContinuationFrames: maximumSequentialContinuationFrames + ) else { newState = .accumulatingHeaderBlockFragments(state) return .needMoreData } diff --git a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift index d6083fc6..e4756ff2 100644 --- a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift +++ b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift @@ -133,6 +133,8 @@ class ConnectionStateMachineTests: XCTestCase { var clientEncoder: HTTP2FrameEncoder! var clientDecoder: HTTP2FrameDecoder! + let maximumSequentialContinuationFrames: Int = 5 + static let requestHeaders = { return HPACKHeaders([(":method", "GET"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")]) }() @@ -150,9 +152,17 @@ class ConnectionStateMachineTests: XCTestCase { self.client = .init(role: .client) self.serverEncoder = HTTP2FrameEncoder(allocator: ByteBufferAllocator()) - self.serverDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: true) + self.serverDecoder = HTTP2FrameDecoder( + allocator: ByteBufferAllocator(), + expectClientMagic: true, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) self.clientEncoder = HTTP2FrameEncoder(allocator: ByteBufferAllocator()) - self.clientDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: false) + self.clientDecoder = HTTP2FrameDecoder( + allocator: ByteBufferAllocator(), + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) } private func exchangePreamble(client: HTTP2Settings = HTTP2Settings(), server: HTTP2Settings = HTTP2Settings()) { diff --git a/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift b/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift index 98e4b49f..c29c7492 100644 --- a/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift +++ b/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift @@ -28,7 +28,9 @@ class HTTP2FrameParserTests: XCTestCase { (":authority", "www.example.com") ]) let simpleHeadersEncoded: [UInt8] = [0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff] - + + let maximumSequentialContinuationFrames = 5 + // MARK: - Utilities private func byteBuffer(withBytes bytes: C, extraCapacity: Int = 0) -> ByteBuffer where C.Element == UInt8 { @@ -116,7 +118,11 @@ class HTTP2FrameParserTests: XCTestCase { file: StaticString = #filePath, line: UInt = #line) throws { let totalFrameSize = bytes.readableBytes - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: bytes) let (frame, actualLength) = try decoder.nextFrame()! @@ -220,7 +226,11 @@ class HTTP2FrameParserTests: XCTestCase { let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: frameBuffer) let (frame, actualLength) = try decoder.nextFrame()! @@ -253,8 +263,12 @@ class HTTP2FrameParserTests: XCTestCase { let expectedFrame2 = HTTP2Frame(streamID: HTTP2StreamID(1), payload: .data(.init(data: .byteBuffer(payloadHalf), endStream: true))) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + let slice = buf.readSlice(length: buf.readableBytes - payloadHalf.readableBytes)! decoder.append(bytes: slice) let frame: HTTP2Frame! = try decoder.nextFrame()?.0 @@ -289,8 +303,12 @@ class HTTP2FrameParserTests: XCTestCase { let expectedFrame2 = HTTP2Frame(streamID: HTTP2StreamID(1), payload: .data(.init(data: .byteBuffer(payloadHalf), endStream: true, paddingBytes: 4))) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + let slice = buf.readSlice(length: 9)! decoder.append(bytes: slice) let noFrame: HTTP2Frame! = try decoder.nextFrame()?.0 @@ -400,7 +418,11 @@ class HTTP2FrameParserTests: XCTestCase { let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: frameBuffer) let (frame, actualLength) = try decoder.nextFrame()! @@ -483,8 +505,12 @@ class HTTP2FrameParserTests: XCTestCase { 0x00, // payload ] let badFrameBuf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + decoder.append(bytes: badFrameBuf) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in guard let connErr = err as? InternalError, case .codecError(code: .protocolError) = connErr else { @@ -504,8 +530,12 @@ class HTTP2FrameParserTests: XCTestCase { // no payload! ] let buf = self.byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + decoder.append(bytes: buf) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in guard let connErr = err as? InternalError, case .codecError(code: .protocolError) = connErr else { @@ -524,7 +554,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in @@ -543,7 +577,11 @@ class HTTP2FrameParserTests: XCTestCase { 0x00, 0x00, 0x00, 0x01, // 4-byte stream identifier ] let badFrameBuf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) XCTAssertEqual(decoder.maxFrameSize, 16384) decoder.append(bytes: badFrameBuf) @@ -571,7 +609,11 @@ class HTTP2FrameParserTests: XCTestCase { let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)]) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: frameBuffer) var (frame, actualLength) = try decoder.nextFrame()! @@ -679,8 +721,12 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBytes(self.simpleHeadersEncoded) buf.writeBytes([UInt8](repeating: 0, count: 3)) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // should fail if the stream is zero buf.setInteger(UInt8(0), at: 8) decoder.append(bytes: buf) @@ -717,7 +763,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buf = self.byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buf) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in @@ -794,7 +844,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in @@ -815,7 +869,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in @@ -900,8 +958,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // cannot be on root stream buf.setInteger(UInt8(0), at: 8) decoder.append(bytes: buf) @@ -975,8 +1037,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // cannot be on root stream buf.setInteger(UInt8(0), at: 8) decoder.append(bytes: buf) @@ -1106,8 +1172,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST be sent on the root stream buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -1183,8 +1253,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST be sent on the root stream buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -1278,8 +1352,12 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes) buf.writeBytes(self.simpleHeadersEncoded) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST NOT be sent on the root stream buf.setInteger(UInt8(0), at: 8) decoder.append(bytes: buf) @@ -1364,7 +1442,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in @@ -1386,7 +1468,11 @@ class HTTP2FrameParserTests: XCTestCase { ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a frame size error", { err in @@ -1447,8 +1533,12 @@ class HTTP2FrameParserTests: XCTestCase { 0x04, 0x05, 0x06, 0x07, ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST NOT be associated with a stream. buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -1558,8 +1648,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST NOT be associated with a stream. buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -1645,8 +1739,12 @@ class HTTP2FrameParserTests: XCTestCase { ] var buf = byteBuffer(withBytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // must have a size of 4 octets buf.setInteger(UInt8(5), at: 2) buf.writeInteger(UInt8(0)) // append an extra byte so we read it all @@ -1697,8 +1795,12 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10) buf.writeBuffer(&headers1) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) XCTAssertNil(try decoder.nextFrame()) @@ -1740,8 +1842,12 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10) buf.writeBuffer(&headers1) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) XCTAssertNil(try decoder.nextFrame()) @@ -1777,7 +1883,11 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10) buf.writeBuffer(&headers) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) // should throw decoder.append(bytes: buf) @@ -1799,7 +1909,11 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10) buf.writeBuffer(&headers1) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) @@ -1836,7 +1950,11 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10) buf.writeBuffer(&headers1) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) @@ -1878,7 +1996,11 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBuffer(&headers1) buf.writeRepeatingByte(0, count: 2) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) @@ -1923,7 +2045,11 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBuffer(&headers1) buf.writeRepeatingByte(0, count: 5) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag decoder.append(bytes: buf) @@ -1947,7 +2073,62 @@ class HTTP2FrameParserTests: XCTestCase { self.assertEqualFrames(frame, expectedFrame) } - + + func testMaximumSequentialContinuationFrames() throws { + let continuationBytes: [UInt8] = [ + // CONTINUATION frame with the END_HEADERS flag not set + 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) + 0x09, // 1-byte frame type (CONTINUATION) + 0x00, // 1-byte flags (none) + 0x00, 0x00, 0x00, 0x03, // 4-byte stream identifier + ] + + var frameBytes: [UInt8] = [ + // HEADERS + 0x00, 0x00, 0x01, // 3-byte payload length (1 byte) + 0x01, // 1-byte frame type (HEADERS) + 0x08, // 1-byte flags (PADDED) + 0x00, 0x00, 0x00, 0x03, // 4-byte stream identifier + 0x00, // 1-byte padding length (0) + // CONTINUATION frame with the END_HEADERS flag set + 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) + 0x09, // 1-byte frame type (CONTINUATION) + 0x04, // 1-byte flags (END_HEADERS) + 0x00, 0x00, 0x00, 0x03 // 4-byte stream identifier + ] + + let excessContinuationFrames = 1 + + // Iteratively test that sequential CONTINUATION frames are received up to the configured + // limit, after which an error should be thrown. + for numberOfContinuationFrames in 1 ... self.maximumSequentialContinuationFrames + excessContinuationFrames { + let buf = byteBuffer(withBytes: frameBytes) + + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + decoder.append(bytes: buf) + + if numberOfContinuationFrames <= self.maximumSequentialContinuationFrames { + let expectedFrame = HTTP2Frame( + streamID: HTTP2StreamID(3), + payload: .headers(.init(headers: [:], endStream: false, paddingBytes: 0)) + ) + try assertReadsFrame(from: buf, matching: expectedFrame) + } else { + XCTAssertThrowsError(try decoder.nextFrame(), "Should throw an 'Excessive CONTINUATION frames' error") { err in + XCTAssert(err is NIOHTTP2Errors.ExcessiveContinuationFrames) + } + } + + // The CONTINUATION frame with the END_HEADERS flag not set will be inserted just before + // the CONTINUATION frame with the END_HEADERS flag set. + frameBytes.insert(contentsOf: continuationBytes, at: frameBytes.endIndex - continuationBytes.count) + } + } + // MARK: - ALTSVC frames func testAltServiceFrameDecoding() throws { @@ -1989,8 +2170,12 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBytes(field.readableBytesView) XCTAssertEqual(buf.readableBytes, 30) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // cannot have origin on a non-root stream buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -2042,7 +2227,11 @@ class HTTP2FrameParserTests: XCTestCase { 0x00, 0x09, // 2-byte origin length ] let buffer = self.allocator.buffer(bytes: frameBytes) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buffer) XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a frame size error") { err in @@ -2093,8 +2282,12 @@ class HTTP2FrameParserTests: XCTestCase { } XCTAssertEqual(buf.readableBytes, 51) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) - + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) + // MUST be sent on root stream (else ignored) buf.setInteger(UInt8(1), at: 8) decoder.append(bytes: buf) @@ -2179,7 +2372,11 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBuffer(&headers4) // This should now yield a HEADERS frame containing the complete set of headers - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buf) let frame: HTTP2Frame! = try decoder.nextFrame()?.0 XCTAssertNotNil(frame) @@ -2226,7 +2423,11 @@ class HTTP2FrameParserTests: XCTestCase { buf.writeBuffer(&headers4) // This should now yield a HEADERS frame containing the complete set of headers - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buf) let frame: HTTP2Frame! = try decoder.nextFrame()?.0 XCTAssertNotNil(frame) @@ -2295,7 +2496,11 @@ class HTTP2FrameParserTests: XCTestCase { let dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.init(data: .byteBuffer(dataBuf), endStream: true))) let goawayFrame = HTTP2Frame(streamID: .rootStream, payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: dataBuf)) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) decoder.append(bytes: buf) let frame1 = try decoder.nextFrame()?.0 @@ -2331,7 +2536,11 @@ class HTTP2FrameParserTests: XCTestCase { var buf = byteBuffer(withBytes: frameBytes, extraCapacity: payload.readableBytes) buf.writeBytes(payload.readableBytesView) - var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var decoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames + ) var slice = buf.readSlice(length: 10)! decoder.append(bytes: slice) guard let result = try assertNoThrowWithValue(decoder.nextFrame()) else { diff --git a/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift b/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift index c077aab5..68b1f3e5 100644 --- a/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift +++ b/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift @@ -36,7 +36,11 @@ typealias IODataWriteRecorder = WriteRecorder extension IODataWriteRecorder { func drainConnectionSetupWrites(mode: NIOHTTP2Handler.ParserMode = .server) throws { - var frameDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: mode == .client) + var frameDecoder = HTTP2FrameDecoder( + allocator: ByteBufferAllocator(), + expectClientMagic: mode == .client, + maximumSequentialContinuationFrames: 5 + ) while self.flushedWrites.count > 0 { let write = self.flushedWrites.removeFirst() @@ -287,7 +291,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { let expectedFrames = streamIDs.map { HTTP2Frame(streamID: $0, payload: .rstStream(.cancel)) } XCTAssertEqual(expectedFrames.count, frameReceiver.flushedWrites.count) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) for (idx, expectedFrame) in expectedFrames.enumerated() { if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[idx] { frameDecoder.append(bytes: flushedWriteBuffer) @@ -328,7 +336,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { // Now we close it. This triggers a RST_STREAM frame. childChannel.close(promise: nil) XCTAssertEqual(frameReceiver.flushedWrites.count, 1) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] { frameDecoder.append(bytes: flushedWriteBuffer) } @@ -369,7 +381,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { let closed = ManagedAtomic(false) childChannel.close().whenComplete { _ in closed.store(true, ordering: .sequentiallyConsistent) } XCTAssertEqual(frameReceiver.flushedWrites.count, 1) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] { frameDecoder.append(bytes: flushedWriteBuffer) } @@ -426,7 +442,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { thirdClosed.store(true, ordering: .sequentiallyConsistent) } XCTAssertEqual(frameReceiver.flushedWrites.count, 1) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] { frameDecoder.append(bytes: flushedWriteBuffer) } @@ -470,7 +490,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { } } XCTAssertEqual(frameReceiver.flushedWrites.count, 1) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] { frameDecoder.append(bytes: flushedWriteBuffer) } @@ -576,7 +600,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase { XCTAssertEqual(frameRecorder.receivedFrames.count, 0) XCTAssertFalse(childChannel.isActive) XCTAssertEqual(writeRecorder.flushedWrites.count, 1) - var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: channel.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) if case .byteBuffer(let flushedWriteBuffer) = writeRecorder.flushedWrites[0] { frameDecoder.append(bytes: flushedWriteBuffer) } diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift index 4e8d814c..db28c576 100644 --- a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift +++ b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift @@ -201,13 +201,16 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase { func basicHTTP2Connection(clientSettings: HTTP2Settings = nioDefaultSettings, serverSettings: HTTP2Settings = nioDefaultSettings, maximumBufferedControlFrames: Int = 10000, + maximumSequentialContinuationFrames: Int = 5, withMultiplexerCallback multiplexerCallback: NIOChannelInitializer? = nil) throws { XCTAssertNoThrow(try self.clientChannel.pipeline.addHandler(NIOHTTP2Handler(mode: .client, initialSettings: clientSettings, - maximumBufferedControlFrames: maximumBufferedControlFrames)).wait()) + maximumBufferedControlFrames: maximumBufferedControlFrames, + maximumSequentialContinuationFrames: maximumSequentialContinuationFrames)).wait()) XCTAssertNoThrow(try self.serverChannel.pipeline.addHandler(NIOHTTP2Handler(mode: .server, initialSettings: serverSettings, - maximumBufferedControlFrames: maximumBufferedControlFrames)).wait()) + maximumBufferedControlFrames: maximumBufferedControlFrames, + maximumSequentialContinuationFrames: maximumSequentialContinuationFrames)).wait()) if let multiplexerCallback = multiplexerCallback { XCTAssertNoThrow(try self.clientChannel.pipeline.addHandler(HTTP2StreamMultiplexer(mode: .client, @@ -1706,7 +1709,9 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase { func testForbidsExceedingMaxHeaderListSizeBeforeDecoding() throws { // Begin by getting the connection up. - try self.basicHTTP2Connection() + // Ensure that the limit for the number of sequential CONTINUATION frames is enough to + // accommodate exceeding the set max header list size. + try self.basicHTTP2Connection(maximumSequentialContinuationFrames: 225) // The server is going to shrink its value for max header list size. let newSettings = [HTTP2Setting(parameter: .maxHeaderListSize, value: 225)] @@ -1789,6 +1794,63 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase { try self.clientChannel.assertReceivedFrame().assertGoAwayFrame(lastStreamID: .maxID, errorCode: UInt32(HTTP2ErrorCode.protocolError.networkCode), opaqueData: nil) } + func testForbidsExceedingMaximumSequentialContinuationFrames() throws { + let maximumSequentialContinuationFrames = 5 + + // Begin by getting the connection up. + try self.basicHTTP2Connection( + maximumSequentialContinuationFrames: maximumSequentialContinuationFrames + ) + + let headersFrame: [UInt8] = [ + 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) + 0x01, // 1-byte frame type (HEADERS) + 0x00, // 1-byte flags (none) + 0x00, 0x00, 0x00, 0x03, // 4-byte stream identifier + ] + let continuationFrame: [UInt8] = [ + 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) + 0x09, // 1-byte frame type (CONTINUATION) + 0x00, // 1-byte flags (none) + 0x00, 0x00, 0x00, 0x03 // 4-byte stream identifier + ] + + var firstBuffer = self.serverChannel.allocator.buffer(capacity: 128) + firstBuffer.writeBytes(headersFrame) + for _ in 0.. [HTTP2Frame] { var receivedFrames: [HTTP2Frame] = Array() - var frameDecoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false) + var frameDecoder = HTTP2FrameDecoder( + allocator: self.allocator, + expectClientMagic: false, + maximumSequentialContinuationFrames: 5 + ) while let buffer = try assertNoThrowWithValue(self.readOutbound(as: ByteBuffer.self), file: (file), line: line) { frameDecoder.append(bytes: buffer) if let (frame, _) = try frameDecoder.nextFrame() {