Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RFC8441 Extended CONNECT #441

Merged
merged 4 commits into from
Jun 24, 2024
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
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