Skip to content

Commit c2c39c6

Browse files
Lukasaweissi
authored andcommitted
Basic PRIORITY frame validation. (#71)
Motivation: In the future we'll want to use PRIORITY frames to provide feedback on how we emit data. Additionally, RFC 7540 forbids certain kinds of PRIORITY frame, which right now we tolerate receiving. We can design a minor PRIORITY shim to ensure that we have a code path for PRIORITY data to be passed around, and then use this shim to enforce RFC 7540's requirements. Modifications: - Added support for PRIORITY frame data inbound. - Enforce that frames may not establish self-dependency. Result: Better policing of RFC 7540 constraints.
1 parent ed92442 commit c2c39c6

10 files changed

+137
-3
lines changed

Sources/NIOHTTP2/HTTP2ChannelHandler.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ extension NIOHTTP2Handler {
206206
// All frames have one basic processing step: do we send them on, or drop them?
207207
// Some frames have further processing steps, regarding triggering user events or other operations.
208208
// Here we centralise this processing.
209-
let result: StateMachineResultWithEffect
209+
var result: StateMachineResultWithEffect
210210

211211
switch frame.payload {
212212
case .alternativeService, .origin:
@@ -218,6 +218,16 @@ extension NIOHTTP2Handler {
218218
result = self.stateMachine.receiveGoaway(lastStreamID: lastStreamID)
219219
case .headers(let headerBody):
220220
result = self.stateMachine.receiveHeaders(streamID: frame.streamID, headers: headerBody.headers, isEndStreamSet: headerBody.endStream)
221+
222+
// Apply a priority update if one is here. If this fails, it may cause a connection error.
223+
if let priorityData = headerBody.priorityData {
224+
do {
225+
try self.outboundBuffer.priorityUpdate(streamID: frame.streamID, priorityData: priorityData)
226+
} catch {
227+
result = StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
228+
}
229+
}
230+
221231
case .ping(let pingData, let ack):
222232
let (stateMachineResult, postPingOperation) = self.stateMachine.receivePing(ackFlagSet: ack)
223233
result = stateMachineResult
@@ -230,8 +240,17 @@ extension NIOHTTP2Handler {
230240
self.encodeAndWriteFrame(context: context, frame: responseFrame, promise: nil)
231241
self.wroteAutomaticFrame = true
232242
}
233-
case .priority:
243+
244+
case .priority(let priorityData):
234245
result = self.stateMachine.receivePriority()
246+
247+
// Apply a priority update if one is here. If this fails, it may cause a connection error.
248+
do {
249+
try self.outboundBuffer.priorityUpdate(streamID: frame.streamID, priorityData: priorityData)
250+
} catch {
251+
result = StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)
252+
}
253+
235254
case .pushPromise(let pushedStreamData):
236255
result = self.stateMachine.receivePushPromise(originalStreamID: frame.streamID, childStreamID: pushedStreamData.pushedStreamID, headers: pushedStreamData.headers)
237256
case .rstStream(let reason):

Sources/NIOHTTP2/HTTP2Error.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,22 @@ public enum NIOHTTP2Errors {
184184

185185
/// A :status header was received with an invalid value.
186186
public struct InvalidStatusValue: NIOHTTP2Error {
187-
var value: String
187+
public var value: String
188188

189189
public init(_ value: String) {
190190
self.value = value
191191
}
192192
}
193+
194+
/// A priority update was received that would create a PRIORITY cycle.
195+
public struct PriorityCycle: NIOHTTP2Error {
196+
/// The affected stream ID.
197+
public var streamID: HTTP2StreamID
198+
199+
public init(streamID: HTTP2StreamID) {
200+
self.streamID = streamID
201+
}
202+
}
193203
}
194204

195205

Sources/NIOHTTP2/OutboundFlowControlBuffer.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,20 @@ internal struct OutboundFlowControlBuffer {
176176
}
177177

178178

179+
// MARK: Priority API
180+
extension OutboundFlowControlBuffer {
181+
/// A frame with new priority data has been received that affects prioritisation of outbound frames.
182+
internal mutating func priorityUpdate(streamID: HTTP2StreamID, priorityData: HTTP2Frame.StreamPriorityData) throws {
183+
// Right now we don't actually do anything with priority information. However, we do want to police some parts of
184+
// RFC 7540 § 5.3, where we can, so this hook is already in place for us to extend later.
185+
if streamID == priorityData.dependency {
186+
// Streams may not depend on themselves!
187+
throw NIOHTTP2Errors.PriorityCycle(streamID: streamID)
188+
}
189+
}
190+
}
191+
192+
179193
private struct StreamFlowControlState {
180194
let streamID: HTTP2StreamID
181195
var currentWindowSize: Int

Sources/NIOHTTP2/OutboundFrameBuffer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ extension CompoundOutboundBuffer {
181181
self.flowControlBuffer.initialWindowSizeChanged(delta)
182182
}
183183

184+
mutating func priorityUpdate(streamID: HTTP2StreamID, priorityData: HTTP2Frame.StreamPriorityData) throws {
185+
try self.flowControlBuffer.priorityUpdate(streamID: streamID, priorityData: priorityData)
186+
}
187+
184188
var maxFrameSize: Int {
185189
get {
186190
return self.flowControlBuffer.maxFrameSize

Tests/NIOHTTP2Tests/CompoundOutboundBufferTest+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension CompoundOutboundBufferTest {
3333
("testFlowControlStreamThrows", testFlowControlStreamThrows),
3434
("testDelayedFlowControlStreamErrors", testDelayedFlowControlStreamErrors),
3535
("testBufferedFrameDrops", testBufferedFrameDrops),
36+
("testRejectsPrioritySelfDependency", testRejectsPrioritySelfDependency),
3637
]
3738
}
3839
}

Tests/NIOHTTP2Tests/CompoundOutboundBufferTest.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ final class CompoundOutboundBufferTest: XCTestCase {
208208
}
209209
XCTAssertEqual(results, [true, false, false, false])
210210
}
211+
212+
func testRejectsPrioritySelfDependency() {
213+
var buffer = CompoundOutboundBuffer(mode: .client, initialMaxOutboundStreams: 1)
214+
215+
XCTAssertThrowsError(try buffer.priorityUpdate(streamID: 1, priorityData: .init(exclusive: false, dependency: 1, weight: 36))) { error in
216+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
217+
}
218+
XCTAssertThrowsError(try buffer.priorityUpdate(streamID: 1, priorityData: .init(exclusive: true, dependency: 1, weight: 36))) { error in
219+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
220+
}
221+
}
211222
}
212223

213224

Tests/NIOHTTP2Tests/OutboundFlowControlBufferTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ extension OutboundFlowControlBufferTests {
3939
("testOverlargeFramesAreSplitOnMaxFrameSizeFileRegion", testOverlargeFramesAreSplitOnMaxFrameSizeFileRegion),
4040
("testChangingStreamWindowSizeToZeroAndBack", testChangingStreamWindowSizeToZeroAndBack),
4141
("testStreamWindowChanges", testStreamWindowChanges),
42+
("testRejectsPrioritySelfDependency", testRejectsPrioritySelfDependency),
4243
]
4344
}
4445
}

Tests/NIOHTTP2Tests/OutboundFlowControlBufferTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,15 @@ class OutboundFlowControlBufferTests: XCTestCase {
371371
self.buffer.flushReceived()
372372
XCTAssertNil(self.buffer.nextFlushedWritableFrame())
373373
}
374+
375+
func testRejectsPrioritySelfDependency() {
376+
XCTAssertThrowsError(try self.buffer.priorityUpdate(streamID: 1, priorityData: .init(exclusive: false, dependency: 1, weight: 36))) { error in
377+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
378+
}
379+
XCTAssertThrowsError(try self.buffer.priorityUpdate(streamID: 1, priorityData: .init(exclusive: true, dependency: 1, weight: 36))) { error in
380+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
381+
}
382+
}
374383
}
375384

376385

Tests/NIOHTTP2Tests/SimpleClientServerTests+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ extension SimpleClientServerTests {
5353
("testSettingsAckNotifiesAboutChangedFlowControl", testSettingsAckNotifiesAboutChangedFlowControl),
5454
("testStreamMultiplexerAcknowledgesSettingsBasedFlowControlChanges", testStreamMultiplexerAcknowledgesSettingsBasedFlowControlChanges),
5555
("testChangingMaxFrameSize", testChangingMaxFrameSize),
56+
("testStreamErrorOnSelfDependentPriorityFrames", testStreamErrorOnSelfDependentPriorityFrames),
57+
("testStreamErrorOnSelfDependentHeadersFrames", testStreamErrorOnSelfDependentHeadersFrames),
5658
]
5759
}
5860
}

Tests/NIOHTTP2Tests/SimpleClientServerTests.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,4 +1193,67 @@ class SimpleClientServerTests: XCTestCase {
11931193
XCTAssertNoThrow(try self.clientChannel.finish())
11941194
XCTAssertNoThrow(try self.serverChannel.finish())
11951195
}
1196+
1197+
func testStreamErrorOnSelfDependentPriorityFrames() throws {
1198+
// Begin by getting the connection up.
1199+
try self.basicHTTP2Connection()
1200+
1201+
// Send a PRIORITY frame that has a self-dependency. Note that we aren't policing these outbound:
1202+
// as we can't detect cycles generally without maintaining a bunch of state, we simply don't. That
1203+
// allows us to emit this.
1204+
let frame = HTTP2Frame(streamID: 1, payload: .priority(.init(exclusive: false, dependency: 1, weight: 32)))
1205+
self.clientChannel.writeAndFlush(frame, promise: nil)
1206+
1207+
// We treat this as a connection error.
1208+
guard let frameData = try assertNoThrowWithValue(self.clientChannel.readOutbound(as: ByteBuffer.self)) else {
1209+
XCTFail("Did not receive frame")
1210+
return
1211+
}
1212+
XCTAssertThrowsError(try self.serverChannel.writeInbound(frameData)) { error in
1213+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
1214+
}
1215+
1216+
guard let responseFrame = try assertNoThrowWithValue(self.serverChannel.readOutbound(as: ByteBuffer.self)) else {
1217+
XCTFail("Did not receive response frame")
1218+
return
1219+
}
1220+
XCTAssertNoThrow(try self.clientChannel.writeInbound(responseFrame))
1221+
1222+
try self.clientChannel.assertReceivedFrame().assertGoAwayFrame(lastStreamID: .maxID, errorCode: UInt32(http2ErrorCode: .protocolError), opaqueData: nil)
1223+
1224+
XCTAssertNoThrow(XCTAssertTrue(try self.clientChannel.finish().isClean))
1225+
_ = try? self.serverChannel.finish()
1226+
}
1227+
1228+
func testStreamErrorOnSelfDependentHeadersFrames() throws {
1229+
// Begin by getting the connection up.
1230+
try self.basicHTTP2Connection()
1231+
1232+
// Send a HEADERS frame that has a self-dependency. Note that we aren't policing these outbound:
1233+
// as we can't detect cycles generally without maintaining a bunch of state, we simply don't. That
1234+
// allows us to emit this.
1235+
let headers = HPACKHeaders([(":path", "/"), (":method", "POST"), (":scheme", "https"), (":authority", "localhost")])
1236+
let frame = HTTP2Frame(streamID: 1, payload: .headers(.init(headers: headers, priorityData: .init(exclusive: true, dependency: 1, weight: 64))))
1237+
self.clientChannel.writeAndFlush(frame, promise: nil)
1238+
1239+
// We treat this as a connection error.
1240+
guard let frameData = try assertNoThrowWithValue(self.clientChannel.readOutbound(as: ByteBuffer.self)) else {
1241+
XCTFail("Did not receive frame")
1242+
return
1243+
}
1244+
XCTAssertThrowsError(try self.serverChannel.writeInbound(frameData)) { error in
1245+
XCTAssertEqual(error as? NIOHTTP2Errors.PriorityCycle, NIOHTTP2Errors.PriorityCycle(streamID: 1))
1246+
}
1247+
1248+
guard let responseFrame = try assertNoThrowWithValue(self.serverChannel.readOutbound(as: ByteBuffer.self)) else {
1249+
XCTFail("Did not receive response frame")
1250+
return
1251+
}
1252+
XCTAssertNoThrow(try self.clientChannel.writeInbound(responseFrame))
1253+
1254+
try self.clientChannel.assertReceivedFrame().assertGoAwayFrame(lastStreamID: .maxID, errorCode: UInt32(http2ErrorCode: .protocolError), opaqueData: nil)
1255+
1256+
XCTAssertNoThrow(XCTAssertTrue(try self.clientChannel.finish().isClean))
1257+
_ = try? self.serverChannel.finish()
1258+
}
11961259
}

0 commit comments

Comments
 (0)