Skip to content

Commit 8752535

Browse files
glbrnttLukasa
authored andcommitted
Ignore WINDOW_UPDATE on closed streams. (#109)
Motivation: RFC 7540 Section 6.9: "WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag. This means that a receiver could receive a WINDOW_UPDATE frame on a "half-closed (remote)" or "closed" stream. A receiver MUST NOT treat this as an error (see Section 5.1)." Previously WINDOW_UPDATE frames received in this state would be treated as an error. Modifications: - Allow frames to be ignored if they are for a stream which has been closed. - Add a test to verify WINDOW_UPDATE on a closed stream. - Update tests which relied on a stream error in this case. Result: More compliant code.
1 parent 8c991df commit 8752535

File tree

5 files changed

+48
-11
lines changed

5 files changed

+48
-11
lines changed

Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,16 @@ struct ConnectionStreamState {
168168
/// - parameters:
169169
/// - streamID: The ID of the stream to modify.
170170
/// - ignoreRecentlyReset: Whether a recently reset stream should be ignored. Should be set to `true` when receiving frames.
171+
/// - ignoreClosed: Whether a closed stream should be ignored. Should be set to `true` when receiving window update frames.
171172
/// - modifier: A block that will be invoked to modify the stream state, if present.
172173
/// - returns: The result of the state modification, as well as any state change that occurred to the stream.
173174
@inline(__always)
174175
mutating func modifyStreamState(streamID: HTTP2StreamID,
175176
ignoreRecentlyReset: Bool,
177+
ignoreClosed: Bool = false,
176178
_ modifier: (inout HTTP2StreamStateMachine) -> StateMachineResultWithStreamEffect) -> StateMachineResultWithStreamEffect {
177179
guard let result = self.activeStreams[streamID].autoClosingTransform(modifier) else {
178-
return StateMachineResultWithStreamEffect(result: self.streamMissing(streamID: streamID, ignoreRecentlyReset: ignoreRecentlyReset), effect: nil)
180+
return StateMachineResultWithStreamEffect(result: self.streamMissing(streamID: streamID, ignoreRecentlyReset: ignoreRecentlyReset, ignoreClosed: ignoreClosed), effect: nil)
179181
}
180182

181183
if let effect = result.effect, effect.closedStream {
@@ -201,7 +203,7 @@ struct ConnectionStreamState {
201203
_ modifier: (inout HTTP2StreamStateMachine) -> StateMachineResultWithStreamEffect) -> StateMachineResultWithStreamEffect {
202204
guard let result = self.activeStreams[streamID].autoClosingTransform(modifier) else {
203205
// We never ignore recently reset streams here, as this should only ever be used when *sending* frames.
204-
return StateMachineResultWithStreamEffect(result: self.streamMissing(streamID: streamID, ignoreRecentlyReset: false), effect: nil)
206+
return StateMachineResultWithStreamEffect(result: self.streamMissing(streamID: streamID, ignoreRecentlyReset: false, ignoreClosed: false), effect: nil)
205207
}
206208

207209

@@ -297,8 +299,9 @@ struct ConnectionStreamState {
297299
/// - parameters:
298300
/// - streamID: The ID of the missing stream.
299301
/// - ignoreRecentlyReset: Whether a recently reset stream should be ignored.
302+
/// - ignoreClosed: Whether a closed stream should be ignored.
300303
/// - returns: A `StateMachineResult` for this frame error.
301-
private func streamMissing(streamID: HTTP2StreamID, ignoreRecentlyReset: Bool) -> StateMachineResult {
304+
private func streamMissing(streamID: HTTP2StreamID, ignoreRecentlyReset: Bool, ignoreClosed: Bool) -> StateMachineResult {
302305
if ignoreRecentlyReset && self.recentlyResetStreams.contains(streamID) {
303306
return .ignoreFrame
304307
}
@@ -310,8 +313,11 @@ struct ConnectionStreamState {
310313
return .connectionError(underlyingError: NIOHTTP2Errors.NoSuchStream(streamID: streamID), type: .protocolError)
311314
default:
312315
// This stream must have already been closed.
313-
return .connectionError(underlyingError: NIOHTTP2Errors.NoSuchStream(streamID: streamID), type: .streamClosed)
314-
316+
if ignoreClosed {
317+
return .ignoreFrame
318+
} else {
319+
return .connectionError(underlyingError: NIOHTTP2Errors.NoSuchStream(streamID: streamID), type: .streamClosed)
320+
}
315321
}
316322
}
317323

Sources/NIOHTTP2/ConnectionStateMachine/FrameReceivingStates/ReceivingPushPromiseState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extension ReceivingPushPromiseState {
5555
try self.streamState.createRemotelyPushedStream(streamID: childStreamID,
5656
remoteInitialWindowSize: self.remoteInitialWindowSize)
5757

58-
let result = self.streamState.modifyStreamState(streamID: originalStreamID, ignoreRecentlyReset: true) {
58+
let result = self.streamState.modifyStreamState(streamID: originalStreamID, ignoreRecentlyReset: true) {
5959
$0.receivePushPromise(headers: headers, validateHeaderBlock: validateHeaderBlock)
6060
}
6161
return StateMachineResultWithEffect(result, connectionState: self)

Sources/NIOHTTP2/ConnectionStateMachine/FrameReceivingStates/ReceivingWindowUpdateState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ extension ReceivingWindowUpdateState {
4141
}
4242
} else {
4343
// This is an update for a specific stream: it's responsible for policing any errors.
44-
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true) {
44+
let result = self.streamState.modifyStreamState(streamID: streamID, ignoreRecentlyReset: true, ignoreClosed: true) {
4545
$0.receiveWindowUpdate(windowIncrement: increment)
4646
}
4747
return StateMachineResultWithEffect(result, connectionState: self)

Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ extension ConnectionStateMachineTests {
6868
("testDataFramesWithoutEndStream", testDataFramesWithoutEndStream),
6969
("testSendingCompleteRequestBeforeResponse", testSendingCompleteRequestBeforeResponse),
7070
("testWindowUpdateValidity", testWindowUpdateValidity),
71+
("testWindowUpdateOnClosedStream", testWindowUpdateOnClosedStream),
7172
("testWindowIncrementsOfSizeZeroArentOk", testWindowIncrementsOfSizeZeroArentOk),
7273
("testCannotSendDataFrames", testCannotSendDataFrames),
7374
("testChangingInitialWindowSizeLotsOfStreams", testChangingInitialWindowSizeLotsOfStreams),

Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,12 @@ class ConnectionStateMachineTests: XCTestCase {
427427

428428
self.setupServerGoaway(streamsToOpen: [streamOne, streamThree, streamFive, streamSeven], lastStreamID: streamThree, expectedToClose: [streamFive, streamSeven])
429429

430-
// Server attempts to send on a closed stream fails, and clients reject that attempt as well.
430+
// Server attempts to send on a closed stream fails, and clients ignore that attempt.
431431
// Client attempts to send on a closed stream fails, but the server ignores such frames.
432432
var temporaryServer = self.server!
433433
var temporaryClient = self.client!
434434
assertConnectionError(type: .streamClosed, temporaryServer.sendWindowUpdate(streamID: streamFive, windowIncrement: 15))
435-
assertConnectionError(type: .streamClosed, temporaryClient.receiveWindowUpdate(streamID: streamFive, windowIncrement: 15))
435+
assertIgnored(temporaryClient.receiveWindowUpdate(streamID: streamFive, windowIncrement: 15))
436436

437437
temporaryServer = self.server!
438438
temporaryClient = self.client!
@@ -551,7 +551,7 @@ class ConnectionStateMachineTests: XCTestCase {
551551
self.setupClientGoaway(clientStreamID: streamOne, streamsToOpen: [streamTwo, streamFour, streamSix], lastStreamID: streamTwo, expectedToClose: [streamFour, streamSix])
552552

553553
// Server attempts to send on a closed stream fails, but clients ignore that attempt.
554-
// Client attempts to send on a closed stream fails, and the server rejects such frames.
554+
// Client attempts to send on a closed stream fails, but the server ignores that attempt.
555555
var temporaryServer = self.server!
556556
var temporaryClient = self.client!
557557
assertConnectionError(type: .streamClosed, temporaryServer.sendWindowUpdate(streamID: streamFour, windowIncrement: 15))
@@ -560,7 +560,7 @@ class ConnectionStateMachineTests: XCTestCase {
560560
temporaryServer = self.server!
561561
temporaryClient = self.client!
562562
assertConnectionError(type: .streamClosed, temporaryClient.sendWindowUpdate(streamID: streamFour, windowIncrement: 15))
563-
assertConnectionError(type: .streamClosed, temporaryServer.receiveWindowUpdate(streamID: streamFour, windowIncrement: 15))
563+
assertIgnored(temporaryServer.receiveWindowUpdate(streamID: streamFour, windowIncrement: 15))
564564
}
565565

566566
func testRstStreamOnClosedStreamAfterClientGoaway() {
@@ -1315,6 +1315,36 @@ class ConnectionStateMachineTests: XCTestCase {
13151315
assertStreamError(type: .protocolError, tempClient.receiveWindowUpdate(streamID: streamTwo, windowIncrement: 15))
13161316
}
13171317

1318+
func testWindowUpdateOnClosedStream() {
1319+
let streamOne = HTTP2StreamID(1)
1320+
1321+
self.exchangePreamble()
1322+
1323+
// Client sends a request.
1324+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: false))
1325+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: false))
1326+
1327+
// Server sends a response.
1328+
assertSucceeds(self.server.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.responseHeaders, isEndStreamSet: false))
1329+
assertSucceeds(self.client.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.responseHeaders, isEndStreamSet: false))
1330+
1331+
// Client sends end stream, server receives end stream.
1332+
assertSucceeds(self.client.sendData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
1333+
assertSucceeds(self.server.receiveData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
1334+
1335+
// Client is now half closed (local), server is half closed (remote)
1336+
1337+
// Server sends end stream and is now closed.
1338+
assertSucceeds(self.server.sendData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
1339+
1340+
// Client is half closed and sends window update, the server MUST ignore this.
1341+
assertSucceeds(self.client.sendWindowUpdate(streamID: streamOne, windowIncrement: 10))
1342+
assertIgnored(self.server.receiveWindowUpdate(streamID: streamOne, windowIncrement: 10))
1343+
1344+
// Client receives end stream and closes.
1345+
assertSucceeds(self.client.receiveData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
1346+
}
1347+
13181348
func testWindowIncrementsOfSizeZeroArentOk() {
13191349
let streamOne = HTTP2StreamID(1)
13201350

0 commit comments

Comments
 (0)