Skip to content

Commit 6af2cf6

Browse files
authored
Implement RFC8441 Extended CONNECT (#441)
Implement RFC8441 Extended CONNECT
1 parent 0a3fcea commit 6af2cf6

15 files changed

+237
-30
lines changed

Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift

+29
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ struct HTTP2ConnectionStateMachine {
8787
return self.localSettings.initialWindowSize
8888
}
8989

90+
var remoteSupportsExtendedConnect: Bool {
91+
false
92+
}
93+
9094
init(fromIdle idleState: IdleConnectionState, localSettings settings: HTTP2SettingsState) {
9195
self.role = idleState.role
9296
self.headerBlockValidation = idleState.headerBlockValidation
@@ -117,6 +121,10 @@ struct HTTP2ConnectionStateMachine {
117121
return HTTP2SettingsState.defaultInitialWindowSize
118122
}
119123

124+
var localSupportsExtendedConnect: Bool {
125+
false
126+
}
127+
120128
init(fromIdle idleState: IdleConnectionState, remoteSettings settings: HTTP2SettingsState) {
121129
self.role = idleState.role
122130
self.headerBlockValidation = idleState.headerBlockValidation
@@ -198,6 +206,10 @@ struct HTTP2ConnectionStateMachine {
198206
return self.role == .client
199207
}
200208

209+
var localSupportsExtendedConnect: Bool {
210+
false
211+
}
212+
201213
init(fromPrefaceReceived state: PrefaceReceivedState, lastStreamID: HTTP2StreamID) {
202214
self.role = state.role
203215
self.headerBlockValidation = state.headerBlockValidation
@@ -236,6 +248,10 @@ struct HTTP2ConnectionStateMachine {
236248
return self.role == .server
237249
}
238250

251+
var remoteSupportsExtendedConnect: Bool {
252+
false
253+
}
254+
239255
init(fromPrefaceSent state: PrefaceSentState, lastStreamID: HTTP2StreamID) {
240256
self.role = state.role
241257
self.headerBlockValidation = state.headerBlockValidation
@@ -412,6 +428,14 @@ struct HTTP2ConnectionStateMachine {
412428
var lastLocalStreamID: HTTP2StreamID
413429
var lastRemoteStreamID: HTTP2StreamID
414430

431+
var localSupportsExtendedConnect: Bool {
432+
false
433+
}
434+
435+
var remoteSupportsExtendedConnect: Bool {
436+
false
437+
}
438+
415439
init<PreviousState: LocallyQuiescingState & RemotelyQuiescingState & SendAndReceiveGoawayState & ConnectionStateWithRole & ConnectionStateWithConfiguration>(previousState: PreviousState) {
416440
self.role = previousState.role
417441
self.headerBlockValidation = previousState.headerBlockValidation
@@ -1630,6 +1654,11 @@ extension HTTP2ConnectionStateMachine {
16301654
guard setting._value >= (1 << 14) && setting._value <= ((1 << 24) - 1) else {
16311655
return .connectionError(underlyingError: NIOHTTP2Errors.invalidSetting(setting: setting), type: .protocolError)
16321656
}
1657+
case .enableConnectProtocol:
1658+
// Must be 0 or 1
1659+
guard setting._value <= 1 else {
1660+
return .connectionError(underlyingError: NIOHTTP2Errors.invalidSetting(setting: setting), type: .protocolError)
1661+
}
16331662
default:
16341663
// All other settings have unrestricted ranges.
16351664
break

Sources/NIOHTTP2/ConnectionStateMachine/FrameReceivingStates/ReceivingHeadersState.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import NIOHPACK
1717
/// can validly accept headers.
1818
///
1919
/// This protocol should only be conformed to by states for the HTTP/2 connection state machine.
20-
protocol ReceivingHeadersState: HasFlowControlWindows {
20+
protocol ReceivingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings {
2121
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }
2222

2323
var headerBlockValidation: HTTP2ConnectionStateMachine.ValidationState { get }
@@ -37,19 +37,21 @@ extension ReceivingHeadersState {
3737
let result: StateMachineResultWithStreamEffect
3838
let validateHeaderBlock = self.headerBlockValidation == .enabled
3939
let validateContentLength = self.contentLengthValidation == .enabled
40+
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
41+
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect
4042

4143
if self.role == .server && streamID.mayBeInitiatedBy(.client) {
4244
do {
4345
result = try self.streamState.modifyStreamStateCreateIfNeeded(streamID: streamID, localRole: .server, localInitialWindowSize: self.localInitialWindowSize, remoteInitialWindowSize: self.remoteInitialWindowSize) {
44-
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
46+
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
4547
}
4648
} catch {
4749
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
4850
}
4951
} else {
5052
// HEADERS cannot create streams for servers, so this must be for a stream we already know about.
5153
result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true) {
52-
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
54+
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
5355
}
5456
}
5557

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

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

7781
// At this stage we've quiesced, so the remote peer is not allowed to create new streams.
7882
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true) {
79-
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
83+
$0.receiveHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
8084
}
8185
return StateMachineResultWithEffect(result,
8286
inboundFlowControlWindow: self.inboundFlowControlWindow,

Sources/NIOHTTP2/ConnectionStateMachine/FrameSendingStates/SendingHeadersState.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import NIOHPACK
1818
/// can validly send headers.
1919
///
2020
/// This protocol should only be conformed to by states for the HTTP/2 connection state machine.
21-
protocol SendingHeadersState: HasFlowControlWindows {
21+
protocol SendingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings {
2222
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }
2323

2424
var headerBlockValidation: HTTP2ConnectionStateMachine.ValidationState { get }
@@ -38,22 +38,24 @@ extension SendingHeadersState {
3838
let result: StateMachineResultWithStreamEffect
3939
let validateHeaderBlock = self.headerBlockValidation == .enabled
4040
let validateContentLength = self.contentLengthValidation == .enabled
41+
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
42+
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect
4143

4244
if self.role == .client && streamID.mayBeInitiatedBy(.client) {
4345
do {
4446
result = try self.streamState.modifyStreamStateCreateIfNeeded(streamID: streamID,
4547
localRole: .client,
4648
localInitialWindowSize: self.localInitialWindowSize,
4749
remoteInitialWindowSize: self.remoteInitialWindowSize) {
48-
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
50+
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
4951
}
5052
} catch {
5153
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
5254
}
5355
} else {
5456
// HEADERS cannot create streams for servers, so this must be for a stream we already know about.
5557
result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: false) {
56-
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
58+
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
5759
}
5860
}
5961

@@ -70,14 +72,16 @@ extension SendingHeadersState where Self: RemotelyQuiescingState {
7072
mutating func sendHeaders(streamID: HTTP2StreamID, headers: HPACKHeaders, isEndStreamSet endStream: Bool) -> StateMachineResultWithEffect {
7173
let validateHeaderBlock = self.headerBlockValidation == .enabled
7274
let validateContentLength = self.contentLengthValidation == .enabled
75+
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
76+
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect
7377
if streamID.mayBeInitiatedBy(.client) &&
7478
self.role == .client &&
7579
streamID > self.streamState.lastClientStreamID {
7680
let error = NIOHTTP2Errors.createdStreamAfterGoaway()
7781
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
7882
}
7983
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: false) {
80-
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, isEndStreamSet: endStream)
84+
$0.sendHeaders(headers: headers, validateHeaderBlock: validateHeaderBlock, validateContentLength: validateContentLength, localSupportsExtendedConnect: localSupportsExtendedConnect, remoteSupportsExtendedConnect: remoteSupportsExtendedConnect, isEndStreamSet: endStream)
8185
}
8286
return StateMachineResultWithEffect(result,
8387
inboundFlowControlWindow: self.inboundFlowControlWindow,

Sources/NIOHTTP2/ConnectionStateMachine/HTTP2SettingsState.swift

+5
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ struct HTTP2SettingsState {
7070
return self[.enablePush]!
7171
}
7272

73+
/// The current value of SETTINGS_ENABLE_CONNECT_PROTOCOL
74+
var enableConnectProtocol: UInt32? {
75+
return self[.enableConnectProtocol]
76+
}
77+
7378
/// The default value of SETTINGS_INITIAL_WINDOW_SIZE.
7479
static let defaultInitialWindowSize: UInt32 = 65535
7580

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
protocol HasLocalExtendedConnectSettings {
16+
var localSupportsExtendedConnect: Bool { get }
17+
}
18+
19+
protocol HasRemoteExtendedConnectSettings {
20+
var remoteSupportsExtendedConnect: Bool { get }
21+
}

Sources/NIOHTTP2/ConnectionStateMachine/HasLocalSettings.swift

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ protocol HasLocalSettings {
2525
var inboundFlowControlWindow: HTTP2FlowControlWindow { get set }
2626
}
2727

28+
extension HasLocalExtendedConnectSettings where Self: HasLocalSettings {
29+
var localSupportsExtendedConnect: Bool {
30+
self.localSettings.enableConnectProtocol == 1
31+
}
32+
}
33+
2834
extension HasLocalSettings {
2935
mutating func receiveSettingsAck(frameDecoder: inout HTTP2FrameDecoder) -> StateMachineResultWithEffect {
3036
// We do a little switcheroo here to avoid problems with overlapping accesses to

Sources/NIOHTTP2/ConnectionStateMachine/HasRemoteSettings.swift

+14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ protocol HasRemoteSettings {
2525
var outboundFlowControlWindow: HTTP2FlowControlWindow { get set }
2626
}
2727

28+
extension HasRemoteExtendedConnectSettings where Self: HasRemoteSettings {
29+
var remoteSupportsExtendedConnect: Bool {
30+
self.remoteSettings.enableConnectProtocol == 1
31+
}
32+
}
33+
2834
extension HasRemoteSettings {
2935
mutating func receiveSettingsChange(_ settings: HTTP2Settings, frameEncoder: inout HTTP2FrameEncoder) -> (StateMachineResultWithEffect, PostFrameOperation) {
3036
// We do a little switcheroo here to avoid problems with overlapping accesses to
@@ -65,6 +71,12 @@ extension HasRemoteSettings {
6571
effect.streamWindowSizeChange += Int(delta)
6672
case .maxFrameSize:
6773
effect.newMaxFrameSize = newValue
74+
case .enableConnectProtocol:
75+
// Must not transition from 1 -> 0
76+
if originalValue == 1 && newValue == 0 {
77+
throw NIOHTTP2Errors.invalidSetting(setting: HTTP2Setting(parameter: setting, value: Int(newValue)))
78+
}
79+
effect.enableConnectProtocol = newValue == 1
6880
default:
6981
// No operation required
7082
return
@@ -73,6 +85,8 @@ extension HasRemoteSettings {
7385
return (.init(result: .succeed, effect: .remoteSettingsChanged(effect)), .sendAck)
7486
} catch let err where err is NIOHTTP2Errors.InvalidFlowControlWindowSize {
7587
return (.init(result: .connectionError(underlyingError: err, type: .flowControlError), effect: nil), .nothing)
88+
} catch let err where err is NIOHTTP2Errors.InvalidSetting {
89+
return (.init(result: .connectionError(underlyingError: err, type: .protocolError), effect: nil), .nothing)
7690
} catch {
7791
preconditionFailure("Unexpected error thrown: \(error)")
7892
}

0 commit comments

Comments
 (0)