Skip to content

Commit c12f59f

Browse files
authored
Buffer in the server pipeline configuration (#1564)
Motivation: When not using TLS, the server pipeline configurator inspects the first bytes on a connection to determine whether HTTP1 or HTTP2 is being used and closes the connection if it is determined that neither are. It does this by only parsing the first packet, which may not have enough bytes to make a correct determination. Modifications: - Buffer bytes in the configurator. - Parse the buffered bytes and only close if enough bytes have been received. Result: Better version determination.
1 parent a70e9ad commit c12f59f

File tree

3 files changed

+178
-41
lines changed

3 files changed

+178
-41
lines changed

Sources/GRPC/GRPCServerPipelineConfigurator.swift

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
3333
/// The server configuration.
3434
private let configuration: Server.Configuration
3535

36-
/// Reads which we're holding on to before the pipeline is configured.
37-
private var bufferedReads = CircularBuffer<NIOAny>()
36+
/// A buffer containing the buffered bytes.
37+
private var buffer: ByteBuffer?
3838

3939
/// The current state.
4040
private var state: State
@@ -212,13 +212,17 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
212212
buffer: ByteBuffer,
213213
context: ChannelHandlerContext
214214
) {
215-
if HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer) {
215+
switch HTTPVersionParser.determineHTTPVersion(buffer) {
216+
case .http2:
216217
self.configureHTTP2(context: context)
217-
} else if HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer) {
218+
case .http1:
218219
self.configureHTTP1(context: context)
219-
} else {
220+
case .unknown:
221+
// Neither H2 nor H1 or the length limit has been exceeded.
220222
self.configuration.logger.error("Unable to determine http version, closing")
221223
context.close(mode: .all, promise: nil)
224+
case .notEnoughBytes:
225+
() // Try again with more bytes.
222226
}
223227
}
224228

@@ -268,13 +272,9 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
268272

269273
/// Try to parse the buffered data to determine whether or not HTTP/2 or HTTP/1 should be used.
270274
private func tryParsingBufferedData(context: ChannelHandlerContext) {
271-
guard let first = self.bufferedReads.first else {
272-
// No data buffered yet. We'll try when we read.
273-
return
275+
if let buffer = self.buffer {
276+
self.determineHTTPVersionAndConfigurePipeline(buffer: buffer, context: context)
274277
}
275-
276-
let buffer = self.unwrapInboundIn(first)
277-
self.determineHTTPVersionAndConfigurePipeline(buffer: buffer, context: context)
278278
}
279279

280280
// MARK: - Channel Handler
@@ -312,7 +312,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
312312
}
313313

314314
internal func channelRead(context: ChannelHandlerContext, data: NIOAny) {
315-
self.bufferedReads.append(data)
315+
var buffer = self.unwrapInboundIn(data)
316+
self.buffer.setOrWriteBuffer(&buffer)
316317

317318
switch self.state {
318319
case .notConfigured(alpn: .notExpected),
@@ -335,8 +336,9 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
335336
removalToken: ChannelHandlerContext.RemovalToken
336337
) {
337338
// Forward any buffered reads.
338-
while let read = self.bufferedReads.popFirst() {
339-
context.fireChannelRead(read)
339+
if let buffer = self.buffer {
340+
self.buffer = nil
341+
context.fireChannelRead(self.wrapInboundOut(buffer))
340342
}
341343
context.leavePipeline(removalToken: removalToken)
342344
}
@@ -375,16 +377,64 @@ struct HTTPVersionParser {
375377

376378
/// Determines whether the bytes in the `ByteBuffer` are prefixed with the HTTP/2 client
377379
/// connection preface.
378-
static func prefixedWithHTTP2ConnectionPreface(_ buffer: ByteBuffer) -> Bool {
380+
static func prefixedWithHTTP2ConnectionPreface(_ buffer: ByteBuffer) -> SubParseResult {
379381
let view = buffer.readableBytesView
380382

381383
guard view.count >= HTTPVersionParser.http2ClientMagic.count else {
382384
// Not enough bytes.
383-
return false
385+
return .notEnoughBytes
384386
}
385387

386388
let slice = view[view.startIndex ..< view.startIndex.advanced(by: self.http2ClientMagic.count)]
387-
return slice.elementsEqual(HTTPVersionParser.http2ClientMagic)
389+
return slice.elementsEqual(HTTPVersionParser.http2ClientMagic) ? .accepted : .rejected
390+
}
391+
392+
enum ParseResult: Hashable {
393+
case http1
394+
case http2
395+
case unknown
396+
case notEnoughBytes
397+
}
398+
399+
enum SubParseResult: Hashable {
400+
case accepted
401+
case rejected
402+
case notEnoughBytes
403+
}
404+
405+
private static let maxLengthToCheck = 1024
406+
407+
static func determineHTTPVersion(_ buffer: ByteBuffer) -> ParseResult {
408+
switch Self.prefixedWithHTTP2ConnectionPreface(buffer) {
409+
case .accepted:
410+
return .http2
411+
412+
case .notEnoughBytes:
413+
switch Self.prefixedWithHTTP1RequestLine(buffer) {
414+
case .accepted:
415+
// Not enough bytes to check H2, but enough to confirm H1.
416+
return .http1
417+
case .notEnoughBytes:
418+
// Not enough bytes to check H2 or H1.
419+
return .notEnoughBytes
420+
case .rejected:
421+
// Not enough bytes to check H2 and definitely not H1.
422+
return .notEnoughBytes
423+
}
424+
425+
case .rejected:
426+
switch Self.prefixedWithHTTP1RequestLine(buffer) {
427+
case .accepted:
428+
// Not H2, but H1 is confirmed.
429+
return .http1
430+
case .notEnoughBytes:
431+
// Not H2, but not enough bytes to reject H1 yet.
432+
return .notEnoughBytes
433+
case .rejected:
434+
// Not H2 or H1.
435+
return .unknown
436+
}
437+
}
388438
}
389439

390440
private static let http1_1 = [
@@ -399,29 +449,59 @@ struct HTTPVersionParser {
399449
]
400450

401451
/// Determines whether the bytes in the `ByteBuffer` are prefixed with an HTTP/1.1 request line.
402-
static func prefixedWithHTTP1RequestLine(_ buffer: ByteBuffer) -> Bool {
452+
static func prefixedWithHTTP1RequestLine(_ buffer: ByteBuffer) -> SubParseResult {
403453
var readableBytesView = buffer.readableBytesView
404454

455+
// We don't need to validate the request line, only determine whether we think it's an HTTP1
456+
// request line. Another handler will parse it properly.
457+
405458
// From RFC 2616 § 5.1:
406459
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
407460

408-
// Read off the Method and Request-URI (and spaces).
409-
guard readableBytesView.trimPrefix(to: UInt8(ascii: " ")) != nil,
410-
readableBytesView.trimPrefix(to: UInt8(ascii: " ")) != nil else {
411-
return false
461+
// Get through the first space.
462+
guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else {
463+
let tooLong = buffer.readableBytes > Self.maxLengthToCheck
464+
return tooLong ? .rejected : .notEnoughBytes
465+
}
466+
467+
// Get through the second space.
468+
guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else {
469+
let tooLong = buffer.readableBytes > Self.maxLengthToCheck
470+
return tooLong ? .rejected : .notEnoughBytes
471+
}
472+
473+
// +2 for \r\n
474+
guard readableBytesView.count >= (Self.http1_1.count + 2) else {
475+
return .notEnoughBytes
412476
}
413477

414-
// Read off the HTTP-Version and CR.
415-
guard let versionView = readableBytesView.trimPrefix(to: UInt8(ascii: "\r")) else {
416-
return false
478+
guard let version = readableBytesView.dropPrefix(through: UInt8(ascii: "\r")),
479+
readableBytesView.first == UInt8(ascii: "\n") else {
480+
// If we didn't drop the prefix OR we did and the next byte wasn't '\n', then we had enough
481+
// bytes but the '\r\n' wasn't present: reject this as being HTTP1.
482+
return .rejected
483+
}
484+
485+
return version.elementsEqual(Self.http1_1) ? .accepted : .rejected
486+
}
487+
}
488+
489+
extension Collection where Self == Self.SubSequence, Self.Element: Equatable {
490+
/// Drops the prefix off the collection up to and including the first `separator`
491+
/// only if that separator appears in the collection.
492+
///
493+
/// Returns the prefix up to but not including the separator if it was found, nil otherwise.
494+
mutating func dropPrefix(through separator: Element) -> SubSequence? {
495+
if self.isEmpty {
496+
return nil
417497
}
418498

419-
// Check that the LF followed the CR.
420-
guard readableBytesView.first == UInt8(ascii: "\n") else {
421-
return false
499+
guard let separatorIndex = self.firstIndex(of: separator) else {
500+
return nil
422501
}
423502

424-
// Now check the HTTP version.
425-
return versionView.elementsEqual(HTTPVersionParser.http1_1)
503+
let prefix = self[..<separatorIndex]
504+
self = self[self.index(after: separatorIndex)...]
505+
return prefix
426506
}
427507
}

Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ class GRPCServerPipelineConfiguratorTests: GRPCTestCase {
135135
self.assertHTTP2Handler(isPresent: true)
136136
}
137137

138+
func testHTTP2SetupViaBytesDripFed() {
139+
self.setUp(tls: false)
140+
var bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
141+
var head = bytes.readSlice(length: bytes.readableBytes - 1)!
142+
let tail = bytes.readSlice(length: 1)!
143+
144+
while let slice = head.readSlice(length: 1) {
145+
assertThat(try self.channel.writeInbound(slice), .doesNotThrow())
146+
self.assertConfigurator(isPresent: true)
147+
self.assertHTTP2Handler(isPresent: false)
148+
}
149+
150+
// Final byte.
151+
assertThat(try self.channel.writeInbound(tail), .doesNotThrow())
152+
self.assertConfigurator(isPresent: false)
153+
self.assertHTTP2Handler(isPresent: true)
154+
}
155+
138156
func testHTTP1Dot1SetupViaBytes() {
139157
self.setUp(tls: false)
140158
let bytes = ByteBuffer(staticString: "GET http://www.foo.bar HTTP/1.1\r\n")
@@ -143,6 +161,39 @@ class GRPCServerPipelineConfiguratorTests: GRPCTestCase {
143161
self.assertGRPCWebToHTTP2Handler(isPresent: true)
144162
}
145163

164+
func testHTTP1Dot1SetupViaBytesDripFed() {
165+
self.setUp(tls: false)
166+
var bytes = ByteBuffer(staticString: "GET http://www.foo.bar HTTP/1.1\r\n")
167+
var head = bytes.readSlice(length: bytes.readableBytes - 1)!
168+
let tail = bytes.readSlice(length: 1)!
169+
170+
while let slice = head.readSlice(length: 1) {
171+
assertThat(try self.channel.writeInbound(slice), .doesNotThrow())
172+
self.assertConfigurator(isPresent: true)
173+
self.assertGRPCWebToHTTP2Handler(isPresent: false)
174+
}
175+
176+
// Final byte.
177+
assertThat(try self.channel.writeInbound(tail), .doesNotThrow())
178+
self.assertConfigurator(isPresent: false)
179+
self.assertGRPCWebToHTTP2Handler(isPresent: true)
180+
}
181+
182+
func testUnexpectedInputClosesEventuallyWhenDripFed() {
183+
self.setUp(tls: false)
184+
var bytes = ByteBuffer(repeating: UInt8(ascii: "a"), count: 2048)
185+
186+
while let slice = bytes.readSlice(length: 1) {
187+
assertThat(try self.channel.writeInbound(slice), .doesNotThrow())
188+
self.assertConfigurator(isPresent: true)
189+
self.assertHTTP2Handler(isPresent: false)
190+
self.assertGRPCWebToHTTP2Handler(isPresent: false)
191+
}
192+
193+
self.channel.embeddedEventLoop.run()
194+
assertThat(try self.channel.closeFuture.wait(), .doesNotThrow())
195+
}
196+
146197
func testReadsAreUnbufferedAfterConfiguration() throws {
147198
self.setUp(tls: false)
148199

Tests/GRPCTests/HTTPVersionParserTests.swift

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,64 @@ class HTTPVersionParserTests: GRPCTestCase {
2222

2323
func testHTTP2ExactlyTheRightBytes() {
2424
let buffer = ByteBuffer(string: self.preface)
25-
XCTAssertTrue(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer))
25+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .accepted)
2626
}
2727

2828
func testHTTP2TheRightBytesAndMore() {
2929
var buffer = ByteBuffer(string: self.preface)
3030
buffer.writeRepeatingByte(42, count: 1024)
31-
XCTAssertTrue(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer))
31+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .accepted)
3232
}
3333

3434
func testHTTP2NoBytes() {
3535
let empty = ByteBuffer()
36-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(empty))
36+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(empty), .notEnoughBytes)
3737
}
3838

3939
func testHTTP2NotEnoughBytes() {
4040
var buffer = ByteBuffer(string: self.preface)
4141
buffer.moveWriterIndex(to: buffer.writerIndex - 1)
42-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer))
42+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .notEnoughBytes)
4343
}
4444

4545
func testHTTP2EnoughOfTheWrongBytes() {
4646
let buffer = ByteBuffer(string: String(self.preface.reversed()))
47-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer))
47+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .rejected)
4848
}
4949

5050
func testHTTP1RequestLine() {
5151
let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1\r\n")
52-
XCTAssertTrue(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer))
52+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .accepted)
5353
}
5454

5555
func testHTTP1RequestLineAndMore() {
5656
let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1\r\nMore")
57-
XCTAssertTrue(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer))
57+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .accepted)
5858
}
5959

6060
func testHTTP1RequestLineWithoutCRLF() {
6161
let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1")
62-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer))
62+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .notEnoughBytes)
6363
}
6464

6565
func testHTTP1NoBytes() {
6666
let empty = ByteBuffer()
67-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP1RequestLine(empty))
67+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(empty), .notEnoughBytes)
6868
}
6969

7070
func testHTTP1IncompleteRequestLine() {
7171
let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html")
72-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer))
72+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .notEnoughBytes)
7373
}
7474

7575
func testHTTP1MalformedVersion() {
7676
let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html ptth/1.1\r\n")
77-
XCTAssertFalse(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer))
77+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .rejected)
78+
}
79+
80+
func testTooManyIncorrectBytes() {
81+
let buffer = ByteBuffer(repeating: UInt8(ascii: "\r"), count: 2048)
82+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .rejected)
83+
XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .rejected)
7884
}
7985
}

0 commit comments

Comments
 (0)