Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion Sources/NIOHTTP2/HTTP2ChannelHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -212,6 +218,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
contentLengthValidation: contentLengthValidation,
maximumSequentialEmptyDataFrames: 1,
maximumBufferedControlFrames: 10000,
maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
maximumResetFrameCount: 200,
resetFrameCounterWindow: .seconds(30))
}
Expand Down Expand Up @@ -241,6 +248,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
contentLengthValidation: contentLengthValidation,
maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames,
maximumBufferedControlFrames: maximumBufferedControlFrames,
maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
maximumResetFrameCount: 200,
resetFrameCounterWindow: .seconds(30))

Expand All @@ -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)
}
Expand All @@ -273,6 +282,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
contentLengthValidation: ValidationState,
maximumSequentialEmptyDataFrames: Int,
maximumBufferedControlFrames: Int,
maximumSequentialContinuationFrames: Int,
maximumResetFrameCount: Int,
resetFrameCounterWindow: TimeAmount) {
self._eventLoop = eventLoop
Expand All @@ -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``.
Expand All @@ -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
Expand All @@ -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)) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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
)
Expand All @@ -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() {}
}

Expand Down
25 changes: 25 additions & 0 deletions Sources/NIOHTTP2/HTTP2Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
}
}


Expand Down
46 changes: 39 additions & 7 deletions Sources/NIOHTTP2/HTTP2FrameParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ struct HTTP2FrameDecoder {
header: syntheticHeader,
initialPayload: payloadBytes,
incomingPayload: self.accumulatedBytes,
originalPaddingBytes: self.expectedPadding
originalPaddingBytes: self.expectedPadding,
continuationSequenceCount: 1
)
)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -556,7 +559,8 @@ struct HTTP2FrameDecoder {
header: header,
initialPayload: payload,
incomingPayload: self.continuationPayload,
originalPaddingBytes: self.originalPaddingBytes
originalPaddingBytes: self.originalPaddingBytes,
continuationSequenceCount: self.continuationSequenceCount + 1
)
)
}
Expand All @@ -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 {
Expand All @@ -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)
}

Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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<ParseResult, Error> {
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
}
Expand Down
14 changes: 12 additions & 2 deletions Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")])
}()
Expand All @@ -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()) {
Expand Down
Loading