Skip to content

Commit

Permalink
Implement RFC8441 Extended CONNECT (#441)
Browse files Browse the repository at this point in the history
Implement RFC8441 Extended CONNECT
  • Loading branch information
ehaydenr authored Jun 24, 2024
1 parent 0a3fcea commit 6af2cf6
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ struct HTTP2ConnectionStateMachine {
return self.localSettings.initialWindowSize
}

var remoteSupportsExtendedConnect: Bool {
false
}

init(fromIdle idleState: IdleConnectionState, localSettings settings: HTTP2SettingsState) {
self.role = idleState.role
self.headerBlockValidation = idleState.headerBlockValidation
Expand Down Expand Up @@ -117,6 +121,10 @@ struct HTTP2ConnectionStateMachine {
return HTTP2SettingsState.defaultInitialWindowSize
}

var localSupportsExtendedConnect: Bool {
false
}

init(fromIdle idleState: IdleConnectionState, remoteSettings settings: HTTP2SettingsState) {
self.role = idleState.role
self.headerBlockValidation = idleState.headerBlockValidation
Expand Down Expand Up @@ -198,6 +206,10 @@ struct HTTP2ConnectionStateMachine {
return self.role == .client
}

var localSupportsExtendedConnect: Bool {
false
}

init(fromPrefaceReceived state: PrefaceReceivedState, lastStreamID: HTTP2StreamID) {
self.role = state.role
self.headerBlockValidation = state.headerBlockValidation
Expand Down Expand Up @@ -236,6 +248,10 @@ struct HTTP2ConnectionStateMachine {
return self.role == .server
}

var remoteSupportsExtendedConnect: Bool {
false
}

init(fromPrefaceSent state: PrefaceSentState, lastStreamID: HTTP2StreamID) {
self.role = state.role
self.headerBlockValidation = state.headerBlockValidation
Expand Down Expand Up @@ -412,6 +428,14 @@ struct HTTP2ConnectionStateMachine {
var lastLocalStreamID: HTTP2StreamID
var lastRemoteStreamID: HTTP2StreamID

var localSupportsExtendedConnect: Bool {
false
}

var remoteSupportsExtendedConnect: Bool {
false
}

init<PreviousState: LocallyQuiescingState & RemotelyQuiescingState & SendAndReceiveGoawayState & ConnectionStateWithRole & ConnectionStateWithConfiguration>(previousState: PreviousState) {
self.role = previousState.role
self.headerBlockValidation = previousState.headerBlockValidation
Expand Down Expand Up @@ -1630,6 +1654,11 @@ extension HTTP2ConnectionStateMachine {
guard setting._value >= (1 << 14) && setting._value <= ((1 << 24) - 1) else {
return .connectionError(underlyingError: NIOHTTP2Errors.invalidSetting(setting: setting), type: .protocolError)
}
case .enableConnectProtocol:
// Must be 0 or 1
guard setting._value <= 1 else {
return .connectionError(underlyingError: NIOHTTP2Errors.invalidSetting(setting: setting), type: .protocolError)
}
default:
// All other settings have unrestricted ranges.
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import NIOHPACK
/// can validly accept headers.
///
/// This protocol should only be conformed to by states for the HTTP/2 connection state machine.
protocol ReceivingHeadersState: HasFlowControlWindows {
protocol ReceivingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings {
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }

var headerBlockValidation: HTTP2ConnectionStateMachine.ValidationState { get }
Expand All @@ -37,19 +37,21 @@ extension ReceivingHeadersState {
let result: StateMachineResultWithStreamEffect
let validateHeaderBlock = self.headerBlockValidation == .enabled
let validateContentLength = self.contentLengthValidation == .enabled
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect

if self.role == .server && streamID.mayBeInitiatedBy(.client) {
do {
result = try self.streamState.modifyStreamStateCreateIfNeeded(streamID: streamID, localRole: .server, localInitialWindowSize: self.localInitialWindowSize, remoteInitialWindowSize: self.remoteInitialWindowSize) {
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
} catch {
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
}
} else {
// HEADERS cannot create streams for servers, so this must be for a stream we already know about.
result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true) {
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
}

Expand All @@ -69,14 +71,16 @@ extension ReceivingHeadersState where Self: LocallyQuiescingState {
mutating func receiveHeaders(streamID: HTTP2StreamID, headers: HPACKHeaders, isEndStreamSet endStream: Bool) -> StateMachineResultWithEffect {
let validateHeaderBlock = self.headerBlockValidation == .enabled
let validateContentLength = self.contentLengthValidation == .enabled
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect

if streamID.mayBeInitiatedBy(.client) && streamID > self.lastRemoteStreamID {
return StateMachineResultWithEffect(result: .ignoreFrame, effect: nil)
}

// At this stage we've quiesced, so the remote peer is not allowed to create new streams.
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true) {
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
return StateMachineResultWithEffect(result,
inboundFlowControlWindow: self.inboundFlowControlWindow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import NIOHPACK
/// can validly send headers.
///
/// This protocol should only be conformed to by states for the HTTP/2 connection state machine.
protocol SendingHeadersState: HasFlowControlWindows {
protocol SendingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings {
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }

var headerBlockValidation: HTTP2ConnectionStateMachine.ValidationState { get }
Expand All @@ -38,22 +38,24 @@ extension SendingHeadersState {
let result: StateMachineResultWithStreamEffect
let validateHeaderBlock = self.headerBlockValidation == .enabled
let validateContentLength = self.contentLengthValidation == .enabled
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect

if self.role == .client && streamID.mayBeInitiatedBy(.client) {
do {
result = try self.streamState.modifyStreamStateCreateIfNeeded(streamID: streamID,
localRole: .client,
localInitialWindowSize: self.localInitialWindowSize,
remoteInitialWindowSize: self.remoteInitialWindowSize) {
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
} catch {
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
}
} else {
// HEADERS cannot create streams for servers, so this must be for a stream we already know about.
result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: false) {
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
}

Expand All @@ -70,14 +72,16 @@ extension SendingHeadersState where Self: RemotelyQuiescingState {
mutating func sendHeaders(streamID: HTTP2StreamID, headers: HPACKHeaders, isEndStreamSet endStream: Bool) -> StateMachineResultWithEffect {
let validateHeaderBlock = self.headerBlockValidation == .enabled
let validateContentLength = self.contentLengthValidation == .enabled
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect
if streamID.mayBeInitiatedBy(.client) &&
self.role == .client &&
streamID > self.streamState.lastClientStreamID {
let error = NIOHTTP2Errors.createdStreamAfterGoaway()
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
}
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: false) {
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
}
return StateMachineResultWithEffect(result,
inboundFlowControlWindow: self.inboundFlowControlWindow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ struct HTTP2SettingsState {
return self[.enablePush]!
}

/// The current value of SETTINGS_ENABLE_CONNECT_PROTOCOL
var enableConnectProtocol: UInt32? {
return self[.enableConnectProtocol]
}

/// The default value of SETTINGS_INITIAL_WINDOW_SIZE.
static let defaultInitialWindowSize: UInt32 = 65535

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

protocol HasLocalExtendedConnectSettings {
var localSupportsExtendedConnect: Bool { get }
}

protocol HasRemoteExtendedConnectSettings {
var remoteSupportsExtendedConnect: Bool { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ protocol HasLocalSettings {
var inboundFlowControlWindow: HTTP2FlowControlWindow { get set }
}

extension HasLocalExtendedConnectSettings where Self: HasLocalSettings {
var localSupportsExtendedConnect: Bool {
self.localSettings.enableConnectProtocol == 1
}
}

extension HasLocalSettings {
mutating func receiveSettingsAck(frameDecoder: inout HTTP2FrameDecoder) -> StateMachineResultWithEffect {
// We do a little switcheroo here to avoid problems with overlapping accesses to
Expand Down
14 changes: 14 additions & 0 deletions Sources/NIOHTTP2/ConnectionStateMachine/HasRemoteSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ protocol HasRemoteSettings {
var outboundFlowControlWindow: HTTP2FlowControlWindow { get set }
}

extension HasRemoteExtendedConnectSettings where Self: HasRemoteSettings {
var remoteSupportsExtendedConnect: Bool {
self.remoteSettings.enableConnectProtocol == 1
}
}

extension HasRemoteSettings {
mutating func receiveSettingsChange(_ settings: HTTP2Settings, frameEncoder: inout HTTP2FrameEncoder) -> (StateMachineResultWithEffect, PostFrameOperation) {
// We do a little switcheroo here to avoid problems with overlapping accesses to
Expand Down Expand Up @@ -65,6 +71,12 @@ extension HasRemoteSettings {
effect.streamWindowSizeChange += Int(delta)
case .maxFrameSize:
effect.newMaxFrameSize = newValue
case .enableConnectProtocol:
// Must not transition from 1 -> 0
if originalValue == 1 && newValue == 0 {
throw NIOHTTP2Errors.invalidSetting(setting: HTTP2Setting(parameter: setting, value: Int(newValue)))
}
effect.enableConnectProtocol = newValue == 1
default:
// No operation required
return
Expand All @@ -73,6 +85,8 @@ extension HasRemoteSettings {
return (.init(result: .succeed, effect: .remoteSettingsChanged(effect)), .sendAck)
} catch let err where err is NIOHTTP2Errors.InvalidFlowControlWindowSize {
return (.init(result: .connectionError(underlyingError: err, type: .flowControlError), effect: nil), .nothing)
} catch let err where err is NIOHTTP2Errors.InvalidSetting {
return (.init(result: .connectionError(underlyingError: err, type: .protocolError), effect: nil), .nothing)
} catch {
preconditionFailure("Unexpected error thrown: \(error)")
}
Expand Down
Loading

0 comments on commit 6af2cf6

Please sign in to comment.