Skip to content

Commit b408ca7

Browse files
carolinacassglbrnttLukasa
authored
ContentLengthVerifier Update (#369)
Motivation: Currently http2 ignores content length when handling a response to a HEAD request and handling a 304 response. Modifications: Changing ContentLengthVerifier and any functions called --------- Co-authored-by: George Barnett <[email protected]> Co-authored-by: Cory Benfield <[email protected]>
1 parent 5485390 commit b408ca7

File tree

10 files changed

+294
-103
lines changed

10 files changed

+294
-103
lines changed

Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStreamsState.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ struct ConnectionStreamState {
7777
/// - parameters:
7878
/// - streamID: The ID of the pushed stream.
7979
/// - remoteInitialWindowSize: The initial window size of the remote peer.
80+
/// - requestVerb: the HTTP method used on the request
8081
/// - throws: If the stream ID is invalid.
81-
mutating func createRemotelyPushedStream(streamID: HTTP2StreamID, remoteInitialWindowSize: UInt32) throws {
82+
mutating func createRemotelyPushedStream(streamID: HTTP2StreamID, remoteInitialWindowSize: UInt32, requestVerb: String?) throws {
8283
try self.reserveServerStreamID(streamID)
83-
let streamState = HTTP2StreamStateMachine(receivedPushPromiseCreatingStreamID: streamID, remoteInitialWindowSize: remoteInitialWindowSize)
84+
let streamState = HTTP2StreamStateMachine(receivedPushPromiseCreatingStreamID: streamID, remoteInitialWindowSize: remoteInitialWindowSize, requestVerb: requestVerb)
8485
self.activeStreams.insert(streamState)
8586
}
8687

@@ -93,9 +94,9 @@ struct ConnectionStreamState {
9394
/// - streamID: The ID of the pushed stream.
9495
/// - localInitialWindowSize: Our initial window size..
9596
/// - throws: If the stream ID is invalid.
96-
mutating func createLocallyPushedStream(streamID: HTTP2StreamID, localInitialWindowSize: UInt32) throws {
97+
mutating func createLocallyPushedStream(streamID: HTTP2StreamID, localInitialWindowSize: UInt32, requestVerb: String?) throws {
9798
try self.reserveServerStreamID(streamID)
98-
let streamState = HTTP2StreamStateMachine(sentPushPromiseCreatingStreamID: streamID, localInitialWindowSize: localInitialWindowSize)
99+
let streamState = HTTP2StreamStateMachine(sentPushPromiseCreatingStreamID: streamID, localInitialWindowSize: localInitialWindowSize, requestVerb: requestVerb)
99100
self.activeStreams.insert(streamState)
100101
}
101102

Sources/NIOHTTP2/ConnectionStateMachine/FrameReceivingStates/ReceivingPushPromiseState.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ extension ReceivingPushPromiseState {
5050
}
5151

5252
let validateHeaderBlock = self.headerBlockValidation == .enabled
53-
53+
let requestVerb = headers.first(name: ":method")
5454
do {
5555
try self.streamState.createRemotelyPushedStream(streamID: childStreamID,
56-
remoteInitialWindowSize: self.remoteInitialWindowSize)
56+
remoteInitialWindowSize: self.remoteInitialWindowSize, requestVerb: requestVerb)
5757

5858
let result = self.streamState.modifyStreamState(streamID: originalStreamID, ignoreRecentlyReset: true) {
5959
$0.receivePushPromise(headers: headers, validateHeaderBlock: validateHeaderBlock)

Sources/NIOHTTP2/ConnectionStateMachine/FrameSendingStates/SendingPushPromiseState.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ extension SendingPushPromiseState {
5959
guard case .succeed = result.result else {
6060
return result
6161
}
62-
63-
try self.streamState.createLocallyPushedStream(streamID: childStreamID, localInitialWindowSize: self.localInitialWindowSize)
62+
let requestVerb = headers.first(name: ":method")
63+
try self.streamState.createLocallyPushedStream(streamID: childStreamID, localInitialWindowSize: self.localInitialWindowSize, requestVerb: requestVerb)
6464
return result
6565
} catch {
6666
return StateMachineResultWithEffect(result: .connectionError(underlyingError: error, type: .protocolError), effect: nil)

Sources/NIOHTTP2/ContentLengthVerifier.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,16 @@ extension ContentLengthVerifier {
4848
}
4949

5050
extension ContentLengthVerifier {
51-
internal init(_ headers: HPACKHeaders) throws {
51+
internal init(_ headers: HPACKHeaders, requestMethod: String?) throws {
52+
if let requestMethod = requestMethod {
53+
if let status = headers.first(name: ":status"), status == "304" {
54+
self.expectedContentLength = 0
55+
return
56+
} else if requestMethod == "HEAD" {
57+
self.expectedContentLength = 0
58+
return
59+
}
60+
}
5261
let contentLengths = headers.values(forHeader: "content-length", canonicalForm: true)
5362
var iterator = contentLengths.makeIterator()
5463
guard let first = iterator.next() else {
@@ -82,3 +91,4 @@ extension ContentLengthVerifier: CustomStringConvertible {
8291
return "ContentLengthVerifier(length: \(String(describing: self.expectedContentLength)))"
8392
}
8493
}
94+

Sources/NIOHTTP2/StreamStateMachine.swift

Lines changed: 74 additions & 73 deletions
Large diffs are not rendered by default.

Sources/NIOHTTP2Server/main.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@ final class HTTP1TestServer: ChannelInboundHandler {
2525
public typealias InboundIn = HTTPServerRequestPart
2626
public typealias OutboundOut = HTTPServerResponsePart
2727

28+
private var head: HTTPRequestHead? = nil
29+
2830
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
29-
guard case .end = self.unwrapInboundIn(data) else {
31+
switch self.unwrapInboundIn(data) {
32+
case .head(let head):
33+
self.head = head
34+
return
35+
case .body:
3036
return
37+
case .end:
38+
// Deliberate fallthrough
39+
()
3140
}
3241

42+
let requestHead = self.head!
43+
self.head = nil
44+
3345
// Insert an event loop tick here. This more accurately represents real workloads in SwiftNIO, which will not
3446
// re-entrantly write their response frames.
3547
context.eventLoop.execute {
@@ -39,9 +51,12 @@ final class HTTP1TestServer: ChannelInboundHandler {
3951
headers.add(name: "x-stream-id", value: String(Int(streamID)))
4052
context.channel.write(self.wrapOutboundOut(HTTPServerResponsePart.head(HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: headers))), promise: nil)
4153

42-
var buffer = context.channel.allocator.buffer(capacity: 12)
43-
buffer.writeStaticString("hello")
44-
context.channel.write(self.wrapOutboundOut(HTTPServerResponsePart.body(.byteBuffer(buffer))), promise: nil)
54+
if requestHead.method != .HEAD {
55+
var buffer = context.channel.allocator.buffer(capacity: 12)
56+
buffer.writeStaticString("hello")
57+
context.channel.write(self.wrapOutboundOut(HTTPServerResponsePart.body(.byteBuffer(buffer))), promise: nil)
58+
}
59+
4560
return context.channel.writeAndFlush(self.wrapOutboundOut(HTTPServerResponsePart.end(nil)))
4661
}.whenComplete { _ in
4762
context.close(promise: nil)

Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ extension ConnectionStateMachineTests {
145145
("testWeTolerateOneStreamBeingResetTwice", testWeTolerateOneStreamBeingResetTwice),
146146
("testReceivedAltServiceFramesAreIgnored", testReceivedAltServiceFramesAreIgnored),
147147
("testReceivedOriginFramesAreIgnored", testReceivedOriginFramesAreIgnored),
148+
("testContentLengthForStatus304", testContentLengthForStatus304),
149+
("testContentLengthForStatus304Failure", testContentLengthForStatus304Failure),
150+
("testContentLengthForMethodHead", testContentLengthForMethodHead),
151+
("testContentLengthForHeadFailure", testContentLengthForHeadFailure),
152+
("testPushHeadRequestFailure", testPushHeadRequestFailure),
153+
("testPushHeadRequest", testPushHeadRequest),
148154
]
149155
}
150156
}

Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3001,6 +3001,149 @@ class ConnectionStateMachineTests: XCTestCase {
30013001
assertIgnored(self.client.receiveOrigin(origins: ["one", "two"]))
30023002
assertIgnored(self.server.receiveOrigin(origins: ["one", "two"]))
30033003
}
3004+
3005+
func testContentLengthForStatus304() {
3006+
let streamOne = HTTP2StreamID(1)
3007+
3008+
self.server = .init(role: .server)
3009+
self.client = .init(role: .client)
3010+
3011+
self.exchangePreamble()
3012+
3013+
let responseHeaders = HPACKHeaders([(":status", "304"), ("content-length", "25")])
3014+
3015+
// Set up the connection
3016+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3017+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3018+
3019+
// The server responds
3020+
assertSucceeds(self.server.sendHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3021+
assertSucceeds(self.client.receiveHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3022+
3023+
// Send in 0 bytes over two sets
3024+
assertSucceeds(self.server.sendData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
3025+
assertSucceeds(self.client.receiveData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
3026+
}
3027+
3028+
func testContentLengthForStatus304Failure() {
3029+
let streamOne = HTTP2StreamID(1)
3030+
3031+
self.server = .init(role: .server)
3032+
self.client = .init(role: .client)
3033+
3034+
self.exchangePreamble()
3035+
3036+
let responseHeaders = HPACKHeaders([(":status", "304"), ("content-length", "25")])
3037+
3038+
// Set up the connection
3039+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3040+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3041+
3042+
// The server responds
3043+
assertSucceeds(self.server.sendHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3044+
assertSucceeds(self.client.receiveHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3045+
3046+
// Send in 1 byte over one frame
3047+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.server.sendData(streamID: streamOne, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3048+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.client.receiveData(streamID: streamOne, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3049+
}
3050+
3051+
func testContentLengthForMethodHead() {
3052+
let streamOne = HTTP2StreamID(1)
3053+
3054+
self.server = .init(role: .server)
3055+
self.client = .init(role: .client)
3056+
3057+
self.exchangePreamble()
3058+
3059+
let requestHeaders = HPACKHeaders([(":method", "HEAD"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")])
3060+
let responseHeaders = HPACKHeaders([(":status", "200"), ("content-length", "25")])
3061+
3062+
// Set up the connection
3063+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: requestHeaders, isEndStreamSet: true))
3064+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: requestHeaders, isEndStreamSet: true))
3065+
3066+
// The server responds
3067+
assertSucceeds(self.server.sendHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3068+
assertSucceeds(self.client.receiveHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3069+
3070+
// Send in 0 bytes over one frame
3071+
assertSucceeds(self.server.sendData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
3072+
assertSucceeds(self.client.receiveData(streamID: streamOne, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
3073+
}
3074+
3075+
func testContentLengthForHeadFailure() {
3076+
let streamOne = HTTP2StreamID(1)
3077+
3078+
self.server = .init(role: .server)
3079+
self.client = .init(role: .client)
3080+
3081+
self.exchangePreamble()
3082+
3083+
let requestHeaders = HPACKHeaders([(":method", "HEAD"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")])
3084+
let responseHeaders = HPACKHeaders([(":status", "200"), ("content-length", "25")])
3085+
3086+
// Set up the connection
3087+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: requestHeaders, isEndStreamSet: true))
3088+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: requestHeaders, isEndStreamSet: true))
3089+
3090+
// The server responds
3091+
assertSucceeds(self.server.sendHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3092+
assertSucceeds(self.client.receiveHeaders(streamID: streamOne, headers: responseHeaders, isEndStreamSet: false))
3093+
3094+
// Send in 1 byte over 1 frame
3095+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.server.sendData(streamID: streamOne, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3096+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.client.receiveData(streamID: streamOne, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3097+
}
3098+
3099+
func testPushHeadRequestFailure() {
3100+
let streamOne = HTTP2StreamID(1)
3101+
let streamTwo = HTTP2StreamID(2)
3102+
3103+
self.exchangePreamble()
3104+
3105+
let requestHeaders = HPACKHeaders([(":method", "HEAD"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")])
3106+
let responseHeaders = HPACKHeaders([(":status", "200"), ("content-length", "25")])
3107+
3108+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3109+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3110+
3111+
// Server can push right away
3112+
assertSucceeds(self.server.sendPushPromise(originalStreamID: streamOne, childStreamID: streamTwo, headers: requestHeaders))
3113+
assertSucceeds(self.client.receivePushPromise(originalStreamID: streamOne, childStreamID: streamTwo, headers: requestHeaders))
3114+
3115+
// The server responds
3116+
assertSucceeds(self.server.sendHeaders(streamID: streamTwo, headers: responseHeaders, isEndStreamSet: false))
3117+
assertSucceeds(self.client.receiveHeaders(streamID: streamTwo, headers: responseHeaders, isEndStreamSet: false))
3118+
3119+
// Send in 1 byte over one frame
3120+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.server.sendData(streamID: streamTwo, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3121+
assertStreamError(type: HTTP2ErrorCode.protocolError, self.client.receiveData(streamID: streamTwo, contentLength: 1, flowControlledBytes: 1, isEndStreamSet: true))
3122+
}
3123+
3124+
func testPushHeadRequest() {
3125+
let streamOne = HTTP2StreamID(1)
3126+
let streamTwo = HTTP2StreamID(2)
3127+
3128+
self.exchangePreamble()
3129+
3130+
let requestHeaders = HPACKHeaders([(":method", "HEAD"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")])
3131+
let responseHeaders = HPACKHeaders([(":status", "200"), ("content-length", "25")])
3132+
3133+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3134+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: true))
3135+
3136+
// Server can push right away
3137+
assertSucceeds(self.server.sendPushPromise(originalStreamID: streamOne, childStreamID: streamTwo, headers: requestHeaders))
3138+
assertSucceeds(self.client.receivePushPromise(originalStreamID: streamOne, childStreamID: streamTwo, headers: requestHeaders))
3139+
3140+
// The server responds
3141+
assertSucceeds(self.server.sendHeaders(streamID: streamTwo, headers: responseHeaders, isEndStreamSet: false))
3142+
assertSucceeds(self.client.receiveHeaders(streamID: streamTwo, headers: responseHeaders, isEndStreamSet: false))
3143+
3144+
// Send in 0 bytes over one frame
3145+
assertSucceeds(self.client.receiveData(streamID: streamTwo, contentLength: 0, flowControlledBytes: 0, isEndStreamSet: true))
3146+
}
30043147
}
30053148

30063149

Tests/NIOHTTP2Tests/ContentLengthVerifierTests+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ extension ContentLengthVerifierTests {
3434
("testMinIntLengthHeaderDoesntPanic", testMinIntLengthHeaderDoesntPanic),
3535
("testMaxIntLengthHeaderDoesntPanic", testMaxIntLengthHeaderDoesntPanic),
3636
("testInvalidLengthHeaderValuesThrow", testInvalidLengthHeaderValuesThrow),
37+
("testContentLengthVerifier_whenResponseStatusIs304", testContentLengthVerifier_whenResponseStatusIs304),
38+
("testContentLengthVerifier_whenRequestMethodIsHead", testContentLengthVerifier_whenRequestMethodIsHead),
3739
]
3840
}
3941
}

0 commit comments

Comments
 (0)