From 3c668f5c7e69baaf92af0671b456124532e649b1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 31 Jul 2020 10:29:42 +0100 Subject: [PATCH] Add HTTP2Frame.FramePayload codecs (#222) Motivation: As part of the work for #214 we need new codecs which transform `HTTP2Frame.FramePayload` to and from the appropriate request and response parts. Modifications: - Add `HTTP2FramePayloadToHTTP1ClientCodec` - Add `HTTP2FramePayloadToHTTP1ServerCodec` - Duplicate the HTTP2ToHTTP1CodecTests and update the relevant parts to use payloads instead of frames. - Add relevant test helpers. - Note: the HTTP2 to HTTP1 (frame based) codecs haven't been deprecated: doing so without warnings depends on #221. Result: - We can transform `HTTP2Frame.FramePayload` types to the relevant HTTP client and server request and response types. --- Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift | 130 +++- Tests/LinuxMain.swift | 1 + ...FramePayloadToHTTP1CodecTests+XCTest.swift | 67 ++ .../HTTP2FramePayloadToHTTP1CodecTests.swift | 683 ++++++++++++++++++ .../HTTP2StreamMultiplexerTests.swift | 13 +- Tests/NIOHTTP2Tests/TestUtilities.swift | 100 +-- 6 files changed, 940 insertions(+), 54 deletions(-) create mode 100644 Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift create mode 100644 Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests.swift diff --git a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift index 5f65acbb..0b16a855 100644 --- a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift +++ b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift @@ -16,6 +16,7 @@ import NIO import NIOHTTP1 import NIOHPACK +// MARK: - Client fileprivate struct BaseClientCodec { private let protocolString: String @@ -32,7 +33,7 @@ fileprivate struct BaseClientCodec { /// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as /// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your /// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`. - fileprivate init(httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol, normalizeHTTPHeaders: Bool) { + fileprivate init(httpProtocol: HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol, normalizeHTTPHeaders: Bool) { self.normalizeHTTPHeaders = normalizeHTTPHeaders switch httpProtocol { @@ -109,10 +110,7 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou public typealias OutboundOut = HTTP2Frame /// The HTTP protocol scheme being used on this connection. - public enum HTTPProtocol { - case https - case http - } + public typealias HTTPProtocol = HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol private let streamID: HTTP2StreamID private var baseCodec: BaseClientCodec @@ -170,6 +168,71 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou } } +/// A simple channel handler that translates HTTP/2 concepts into HTTP/1 data types, +/// and vice versa, for use on the client side. +/// +/// This channel handler should be used alongside the `HTTP2StreamMultiplexer` to +/// help provide a HTTP/1.1-like abstraction on top of a HTTP/2 multiplexed +/// connection. +/// +/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type. +public final class HTTP2FramePayloadToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutboundHandler { + public typealias InboundIn = HTTP2Frame.FramePayload + public typealias InboundOut = HTTPClientResponsePart + + public typealias OutboundIn = HTTPClientRequestPart + public typealias OutboundOut = HTTP2Frame.FramePayload + + private var baseCodec: BaseClientCodec + + /// The HTTP protocol scheme being used on this connection. + public enum HTTPProtocol { + case https + case http + } + + /// Initializes a `HTTP2PayloadToHTTP1ClientCodec`. + /// + /// - parameters: + /// - httpProtocol: The protocol (usually `"http"` or `"https"` that is used). + /// - normalizeHTTPHeaders: Whether to automatically normalize the HTTP headers to be suitable for HTTP/2. + /// The normalization will for example lower-case all heder names (as required by the + /// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as + /// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your + /// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`. + public init(httpProtocol: HTTPProtocol, normalizeHTTPHeaders: Bool = true) { + self.baseCodec = BaseClientCodec(httpProtocol: httpProtocol, normalizeHTTPHeaders: normalizeHTTPHeaders) + } + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let payload = self.unwrapInboundIn(data) + do { + let (first, second) = try self.baseCodec.processInboundData(payload) + if let first = first { + context.fireChannelRead(self.wrapInboundOut(first)) + } + if let second = second { + context.fireChannelRead(self.wrapInboundOut(second)) + } + } catch { + context.fireErrorCaught(error) + } + } + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let responsePart = self.unwrapOutboundIn(data) + + do { + let transformedPayload = try self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator) + context.write(self.wrapOutboundOut(transformedPayload), promise: promise) + } catch { + promise?.fail(error) + context.fireErrorCaught(error) + } + } +} + +// MARK: - Server fileprivate struct BaseServerCodec { private let normalizeHTTPHeaders: Bool @@ -297,6 +360,63 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou } } +/// A simple channel handler that translates HTTP/2 concepts into HTTP/1 data types, +/// and vice versa, for use on the server side. +/// +/// This channel handler should be used alongside the `HTTP2StreamMultiplexer` to +/// help provide a HTTP/1.1-like abstraction on top of a HTTP/2 multiplexed +/// connection. +/// +/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type. +public final class HTTP2FramePayloadToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutboundHandler { + public typealias InboundIn = HTTP2Frame.FramePayload + public typealias InboundOut = HTTPServerRequestPart + + public typealias OutboundIn = HTTPServerResponsePart + public typealias OutboundOut = HTTP2Frame.FramePayload + + private var baseCodec: BaseServerCodec + + /// Initializes a `HTTP2PayloadToHTTP1ServerCodec`. + /// + /// - parameters: + /// - normalizeHTTPHeaders: Whether to automatically normalize the HTTP headers to be suitable for HTTP/2. + /// The normalization will for example lower-case all heder names (as required by the + /// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as + /// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your + /// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`. + public init(normalizeHTTPHeaders: Bool = true) { + self.baseCodec = BaseServerCodec(normalizeHTTPHeaders: normalizeHTTPHeaders) + } + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let payload = self.unwrapInboundIn(data) + + do { + let (first, second) = try self.baseCodec.processInboundData(payload) + if let first = first { + context.fireChannelRead(self.wrapInboundOut(first)) + } + if let second = second { + context.fireChannelRead(self.wrapInboundOut(second)) + } + } catch { + context.fireErrorCaught(error) + } + } + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let responsePart = self.unwrapOutboundIn(data) + + do { + let transformedPayload = try self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator) + context.write(self.wrapOutboundOut(transformedPayload), promise: promise) + } catch { + promise?.fail(error) + context.fireErrorCaught(error) + } + } +} private extension HTTPMethod { /// Create a `HTTPMethod` from the string representation of that method. diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 9a570a4f..0ec320a9 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -45,6 +45,7 @@ class LinuxMainRunnerImpl: LinuxMainRunner { testCase(HPACKRegressionTests.allTests), testCase(HTTP2FrameConvertibleTests.allTests), testCase(HTTP2FrameParserTests.allTests), + testCase(HTTP2FramePayloadToHTTP1CodecTests.allTests), testCase(HTTP2StreamMultiplexerTests.allTests), testCase(HTTP2ToHTTP1CodecTests.allTests), testCase(HeaderTableTests.allTests), diff --git a/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift b/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift new file mode 100644 index 00000000..bab7205b --- /dev/null +++ b/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension HTTP2FramePayloadToHTTP1CodecTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (HTTP2FramePayloadToHTTP1CodecTests) -> () throws -> Void)] { + return [ + ("testBasicRequestServerSide", testBasicRequestServerSide), + ("testRequestWithOnlyHeadServerSide", testRequestWithOnlyHeadServerSide), + ("testRequestWithTrailers", testRequestWithTrailers), + ("testSendingSimpleResponse", testSendingSimpleResponse), + ("testResponseWithoutTrailers", testResponseWithoutTrailers), + ("testResponseWith100Blocks", testResponseWith100Blocks), + ("testPassingPromisesThroughWritesOnServer", testPassingPromisesThroughWritesOnServer), + ("testBasicResponseClientSide", testBasicResponseClientSide), + ("testResponseWithOnlyHeadClientSide", testResponseWithOnlyHeadClientSide), + ("testResponseWithTrailers", testResponseWithTrailers), + ("testSendingSimpleRequest", testSendingSimpleRequest), + ("testRequestWithoutTrailers", testRequestWithoutTrailers), + ("testResponseWith100BlocksClientSide", testResponseWith100BlocksClientSide), + ("testPassingPromisesThroughWritesOnClient", testPassingPromisesThroughWritesOnClient), + ("testReceiveRequestWithoutMethod", testReceiveRequestWithoutMethod), + ("testReceiveRequestWithDuplicateMethod", testReceiveRequestWithDuplicateMethod), + ("testReceiveRequestWithoutPath", testReceiveRequestWithoutPath), + ("testReceiveRequestWithDuplicatePath", testReceiveRequestWithDuplicatePath), + ("testReceiveRequestWithoutAuthority", testReceiveRequestWithoutAuthority), + ("testReceiveRequestWithDuplicateAuthority", testReceiveRequestWithDuplicateAuthority), + ("testReceiveRequestWithoutScheme", testReceiveRequestWithoutScheme), + ("testReceiveRequestWithDuplicateScheme", testReceiveRequestWithDuplicateScheme), + ("testReceiveResponseWithoutStatus", testReceiveResponseWithoutStatus), + ("testReceiveResponseWithDuplicateStatus", testReceiveResponseWithDuplicateStatus), + ("testReceiveResponseWithNonNumericalStatus", testReceiveResponseWithNonNumericalStatus), + ("testSendRequestWithoutHost", testSendRequestWithoutHost), + ("testSendRequestWithDuplicateHost", testSendRequestWithDuplicateHost), + ("testFramesWithoutHTTP1EquivalentAreIgnored", testFramesWithoutHTTP1EquivalentAreIgnored), + ("testWeTolerateUpperCasedHTTP1HeadersForRequests", testWeTolerateUpperCasedHTTP1HeadersForRequests), + ("testWeTolerateUpperCasedHTTP1HeadersForResponses", testWeTolerateUpperCasedHTTP1HeadersForResponses), + ("testWeDoNotNormalizeHeadersIfUserAskedUsNotToForRequests", testWeDoNotNormalizeHeadersIfUserAskedUsNotToForRequests), + ("testWeDoNotNormalizeHeadersIfUserAskedUsNotToForResponses", testWeDoNotNormalizeHeadersIfUserAskedUsNotToForResponses), + ("testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForRequests", testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForRequests), + ("testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForResponses", testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForResponses), + ] + } +} + diff --git a/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests.swift b/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests.swift new file mode 100644 index 00000000..604dc557 --- /dev/null +++ b/Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecTests.swift @@ -0,0 +1,683 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest +import NIO +import NIOHTTP1 +import NIOHPACK +@testable import NIOHTTP2 + +final class HTTP2FramePayloadToHTTP1CodecTests: XCTestCase { + var channel: EmbeddedChannel! + + override func setUp() { + self.channel = EmbeddedChannel() + } + + override func tearDown() { + self.channel = nil + } + + func testBasicRequestServerSide() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) + + var expectedRequestHead = HTTPRequestHead(version: HTTPVersion(major: 2, minor: 0), method: .POST, uri: "/post") + expectedRequestHead.headers.add(name: "host", value: "example.org") + expectedRequestHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedServerRequestPart(.head(expectedRequestHead)) + + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + let dataPayload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(bodyData), endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(dataPayload)) + self.channel.assertReceivedServerRequestPart(.body(bodyData)) + self.channel.assertReceivedServerRequestPart(.end(nil)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testRequestWithOnlyHeadServerSide() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/get"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + let headersPayload = HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders, endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) + + var expectedRequestHead = HTTPRequestHead(version: HTTPVersion(major: 2, minor: 0), method: .GET, uri: "/get") + expectedRequestHead.headers.add(name: "host", value: "example.org") + expectedRequestHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedServerRequestPart(.head(expectedRequestHead)) + self.channel.assertReceivedServerRequestPart(.end(nil)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testRequestWithTrailers() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/get"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + let headersPayload = HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)) + XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) + + var expectedRequestHead = HTTPRequestHead(version: HTTPVersion(major: 2, minor: 0), method: .GET, uri: "/get") + expectedRequestHead.headers.add(name: "host", value: "example.org") + expectedRequestHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedServerRequestPart(.head(expectedRequestHead)) + + // Ok, we're going to send trailers. + let trailers = HPACKHeaders([("a trailer", "yes"), ("another trailer", "also yes")]) + let trailersPayload = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(trailersPayload)) + + self.channel.assertReceivedServerRequestPart(.end(HTTPHeaders(regularHeadersFrom: trailers))) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testSendingSimpleResponse() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic response. + let responseHeaders = HPACKHeaders([("server", "swift-nio"), ("other", "header")]) + let responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: HTTPHeaders(regularHeadersFrom: responseHeaders)) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + + let expectedResponseHeaders = HPACKHeaders([(":status", "200")]) + responseHeaders + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, headers: expectedResponseHeaders) + + // Now body. + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + self.channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(bodyData)), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 2) + writeRecorder.flushedWrites[1].assertDataPayload(endStream: false, payload: bodyData) + + // Now trailers. + let trailers = HPACKHeaders([("a-trailer", "yes"), ("another-trailer", "still yes")]) + self.channel.writeAndFlush(HTTPServerResponsePart.end(HTTPHeaders(regularHeadersFrom: trailers)), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 3) + writeRecorder.flushedWrites[2].assertHeadersPayload(endStream: true, headers: trailers) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testResponseWithoutTrailers() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic response. + let responseHeaders = HPACKHeaders([("server", "swift-nio"), ("other", "header")]) + let responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: HTTPHeaders(regularHeadersFrom: responseHeaders)) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + + let expectedResponseHeaders = HPACKHeaders([(":status", "200")]) + responseHeaders + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, headers: expectedResponseHeaders) + + // No trailers, just end. + let emptyBuffer = self.channel.allocator.buffer(capacity: 0) + self.channel.writeAndFlush(HTTPServerResponsePart.end(nil), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 2) + writeRecorder.flushedWrites[1].assertDataPayload(endStream: true, payload: emptyBuffer) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testResponseWith100Blocks() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // First, we're going to send a few 103 blocks. + let informationalResponseHeaders = HPACKHeaders([("link", "no link really")]) + let informationalResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .custom(code: 103, reasonPhrase: "Early Hints"), headers: HTTPHeaders(regularHeadersFrom: informationalResponseHeaders)) + for _ in 0..<3 { + self.channel.write(HTTPServerResponsePart.head(informationalResponseHead), promise: nil) + } + self.channel.flush() + + let expectedInformationalResponseHeaders = HPACKHeaders([(":status", "103")]) + informationalResponseHeaders + XCTAssertEqual(writeRecorder.flushedWrites.count, 3) + for idx in 0..<3 { + writeRecorder.flushedWrites[idx].assertHeadersPayload(endStream: false, headers: expectedInformationalResponseHeaders) + } + + // Now we finish up with a basic response. + let responseHeaders = HPACKHeaders([("server", "swift-nio"), ("other", "header")]) + let responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: HTTPHeaders(regularHeadersFrom: responseHeaders)) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + self.channel.writeAndFlush(HTTPServerResponsePart.end(nil), promise: nil) + + let expectedResponseHeaders = HPACKHeaders([(":status", "200")]) + responseHeaders + let emptyBuffer = self.channel.allocator.buffer(capacity: 0) + XCTAssertEqual(writeRecorder.flushedWrites.count, 5) + writeRecorder.flushedWrites[3].assertHeadersPayload(endStream: false, headers: expectedResponseHeaders) + writeRecorder.flushedWrites[4].assertDataPayload(endStream: true, payload: emptyBuffer) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testPassingPromisesThroughWritesOnServer() throws { + let promiseRecorder = PromiseRecorder() + + let promises: [EventLoopPromise] = (0..<3).map { _ in self.channel.eventLoop.makePromise() } + XCTAssertNoThrow(try self.channel.pipeline.addHandler(promiseRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic response. + let responseHeaders = HTTPHeaders([("server", "swift-nio"), ("other", "header")]) + let responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: responseHeaders) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: promises[0]) + + // Now body. + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + self.channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(bodyData)), promise: promises[1]) + + // Now trailers. + let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "still yes")]) + self.channel.writeAndFlush(HTTPServerResponsePart.end(trailers), promise: promises[2]) + + XCTAssertEqual(promiseRecorder.recordedPromises.count, 3) + for (idx, promise) in promiseRecorder.recordedPromises.enumerated() { + XCTAssertTrue(promise!.futureResult === promises[idx].futureResult) + } + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testBasicResponseClientSide() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) + XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: responseHeaders))))) + + var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + expectedResponseHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedClientResponsePart(.head(expectedResponseHead)) + + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + let dataPayload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(bodyData), endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(dataPayload)) + self.channel.assertReceivedClientResponsePart(.body(bodyData)) + self.channel.assertReceivedClientResponsePart(.end(nil)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testResponseWithOnlyHeadClientSide() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) + let headersPayload = HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: responseHeaders), endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) + + var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + expectedResponseHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedClientResponsePart(.head(expectedResponseHead)) + self.channel.assertReceivedClientResponsePart(.end(nil)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testResponseWithTrailers() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) + let headersPayload = HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: responseHeaders))) + XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) + + var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + expectedResponseHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedClientResponsePart(.head(expectedResponseHead)) + + // Ok, we're going to send trailers. + let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "also yes")]) + let trailersPayload = HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: trailers), endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(trailersPayload)) + + self.channel.assertReceivedClientResponsePart(.end(trailers)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testSendingSimpleRequest() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([("host", "example.org"), ("other", "header")]) + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = HTTPHeaders(regularHeadersFrom: requestHeaders) + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, headers: expectedRequestHeaders) + + // Now body. + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + self.channel.writeAndFlush(HTTPClientRequestPart.body(.byteBuffer(bodyData)), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 2) + writeRecorder.flushedWrites[1].assertDataPayload(endStream: false, payload: bodyData) + + // Now trailers. + let trailers = HPACKHeaders([("a-trailer", "yes"), ("another-trailer", "still yes")]) + self.channel.writeAndFlush(HTTPClientRequestPart.end(HTTPHeaders(regularHeadersFrom: trailers)), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 3) + writeRecorder.flushedWrites[2].assertHeadersPayload(endStream: true, headers: trailers) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testRequestWithoutTrailers() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .http)).wait()) + + // A basic request. + let requestHeaders = HTTPHeaders([("host", "example.org"), ("other", "header")]) + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = requestHeaders + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "http"), (":authority", "example.org"), ("other", "header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, headers: expectedRequestHeaders) + + // No trailers, just end. + let emptyBuffer = self.channel.allocator.buffer(capacity: 0) + self.channel.writeAndFlush(HTTPClientRequestPart.end(nil), promise: nil) + XCTAssertEqual(writeRecorder.flushedWrites.count, 2) + writeRecorder.flushedWrites[1].assertDataPayload(endStream: true, payload: emptyBuffer) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testResponseWith100BlocksClientSide() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // Start with a few 100 blocks. + let informationalResponseHeaders = HTTPHeaders([(":status", "103"), ("link", "example")]) + for _ in 0..<3 { + XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: informationalResponseHeaders))))) + } + + var expectedInformationalResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .custom(code: 103, reasonPhrase: "")) + expectedInformationalResponseHead.headers.add(name: "link", value: "example") + for _ in 0..<3 { + self.channel.assertReceivedClientResponsePart(.head(expectedInformationalResponseHead)) + } + + // Now a response. + let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) + let responsePayload = HTTP2Frame.FramePayload.headers(.init(headers: HPACKHeaders(httpHeaders: responseHeaders), endStream: true)) + XCTAssertNoThrow(try self.channel.writeInbound(responsePayload)) + + var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + expectedResponseHead.headers.add(name: "other", value: "header") + self.channel.assertReceivedClientResponsePart(.head(expectedResponseHead)) + self.channel.assertReceivedClientResponsePart(.end(nil)) + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testPassingPromisesThroughWritesOnClient() throws { + let promiseRecorder = PromiseRecorder() + + let promises: [EventLoopPromise] = (0..<3).map { _ in self.channel.eventLoop.makePromise() } + XCTAssertNoThrow(try self.channel.pipeline.addHandler(promiseRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let requestHeaders = HTTPHeaders([("host", "example.org"), ("other", "header")]) + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = requestHeaders + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: promises[0]) + + // Now body. + var bodyData = self.channel.allocator.buffer(capacity: 12) + bodyData.writeStaticString("hello, world!") + self.channel.writeAndFlush(HTTPClientRequestPart.body(.byteBuffer(bodyData)), promise: promises[1]) + + // Now trailers. + let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "still yes")]) + self.channel.writeAndFlush(HTTPClientRequestPart.end(trailers), promise: promises[2]) + + XCTAssertEqual(promiseRecorder.recordedPromises.count, 3) + for (idx, promise) in promiseRecorder.recordedPromises.enumerated() { + XCTAssertTrue(promise!.futureResult === promises[idx].futureResult) + } + + XCTAssertNoThrow(try self.channel.finish()) + } + + func testReceiveRequestWithoutMethod() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingPseudoHeader, NIOHTTP2Errors.MissingPseudoHeader(":method")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithDuplicateMethod() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":method", "GET"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicatePseudoHeader, NIOHTTP2Errors.DuplicatePseudoHeader(":method")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithoutPath() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingPseudoHeader, NIOHTTP2Errors.MissingPseudoHeader(":path")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithDuplicatePath() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":path", "/post"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicatePseudoHeader, NIOHTTP2Errors.DuplicatePseudoHeader(":path")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithoutAuthority() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":method", "GET"), (":scheme", "https"), (":path", "/post"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingPseudoHeader, NIOHTTP2Errors.MissingPseudoHeader(":authority")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithDuplicateAuthority() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicatePseudoHeader, NIOHTTP2Errors.DuplicatePseudoHeader(":authority")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithoutScheme() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":method", "GET"), (":authority", "example.org"), (":path", "/post"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingPseudoHeader, NIOHTTP2Errors.MissingPseudoHeader(":scheme")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveRequestWithDuplicateScheme() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":path", "/post"), (":method", "GET"), (":scheme", "https"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicatePseudoHeader, NIOHTTP2Errors.DuplicatePseudoHeader(":scheme")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveResponseWithoutStatus() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let requestHeaders = HPACKHeaders([("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingPseudoHeader, NIOHTTP2Errors.MissingPseudoHeader(":status")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveResponseWithDuplicateStatus() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request. + let requestHeaders = HPACKHeaders([(":status", "200"), (":status", "404"), ("other", "header")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicatePseudoHeader, NIOHTTP2Errors.DuplicatePseudoHeader(":status")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testReceiveResponseWithNonNumericalStatus() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic response. + let requestHeaders = HPACKHeaders([(":status", "captivating")]) + XCTAssertThrowsError(try self.channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: requestHeaders)))) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.InvalidStatusValue, NIOHTTP2Errors.InvalidStatusValue("captivating")) + } + + // We already know there's an error here. + _ = try? self.channel.finish() + } + + func testSendRequestWithoutHost() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request without Host. + let request = HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .GET, uri: "/")) + XCTAssertThrowsError(try self.channel.writeOutbound(request)) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingHostHeader, NIOHTTP2Errors.MissingHostHeader()) + } + + // We check the channel for an error as the above only checks the promise. + XCTAssertThrowsError(try self.channel.finish()) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.MissingHostHeader, NIOHTTP2Errors.MissingHostHeader()) + } + } + + func testSendRequestWithDuplicateHost() throws { + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request with too many host headers. + var requestHead = HTTPRequestHead(version: .init(major: 1, minor: 1), method: .GET, uri: "/") + requestHead.headers.add(name: "Host", value: "fish") + requestHead.headers.add(name: "Host", value: "cat") + let request = HTTPClientRequestPart.head(requestHead) + XCTAssertThrowsError(try self.channel.writeOutbound(request)) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicateHostHeader, NIOHTTP2Errors.DuplicateHostHeader()) + } + + // We check the channel for an error as the above only checks the promise. + XCTAssertThrowsError(try self.channel.finish()) { error in + XCTAssertEqual(error as? NIOHTTP2Errors.DuplicateHostHeader, NIOHTTP2Errors.DuplicateHostHeader()) + } + } + + func testFramesWithoutHTTP1EquivalentAreIgnored() throws { + let streamID = HTTP2StreamID(1) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + let headers = HPACKHeaders([(":method", "GET"), (":scheme", "https"), (":path", "/x")]) + let payloads: [HTTP2Frame.FramePayload] = [.alternativeService(origin: nil, field: nil), + .rstStream(.init(networkCode: 1)), + .priority(.init(exclusive: true, dependency: streamID, weight: 1)), + .windowUpdate(windowSizeIncrement: 1), + .settings(.ack), + .pushPromise(.init(pushedStreamID: HTTP2StreamID(2), headers: headers)), + .ping(.init(withInteger: 123), ack: true), + .goAway(lastStreamID: streamID, errorCode: .init(networkCode: 1), opaqueData: nil), + .origin([])] + for payload in payloads { + XCTAssertNoThrow(try self.channel.writeInbound(payload), "error on \(payload)") + } + XCTAssertNoThrow(XCTAssertTrue(try self.channel.finish().isClean)) + } + + func testWeTolerateUpperCasedHTTP1HeadersForRequests() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request. + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = HTTPHeaders([("host", "example.org"), ("UpperCased", "Header")]) + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "https"), (":authority", "example.org"), ("uppercased", "Header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .request) + } + + func testWeTolerateUpperCasedHTTP1HeadersForResponses() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + var responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + responseHead.headers = HTTPHeaders([("UpperCased", "Header")]) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":status", "200"), ("uppercased", "Header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .response) + } + + func testWeDoNotNormalizeHeadersIfUserAskedUsNotToForRequests() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https, + normalizeHTTPHeaders: false)).wait()) + + // A basic request. + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = HTTPHeaders([("host", "example.org"), ("UpperCased", "Header")]) + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "https"), + (":authority", "example.org"), ("UpperCased", "Header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .doNotValidate) + } + + func testWeDoNotNormalizeHeadersIfUserAskedUsNotToForResponses() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec(normalizeHTTPHeaders: false)).wait()) + + var responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + responseHead.headers = HTTPHeaders([("UpperCased", "Header")]) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":status", "200"), ("UpperCased", "Header")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .doNotValidate) + } + + func testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForRequests() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)).wait()) + + // A basic request. + var requestHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .POST, uri: "/post") + requestHead.headers = HTTPHeaders([("host", "example.org"), ("connection", "keep-alive, also-to-be-removed"), + ("keep-alive", "foo"), ("also-to-be-removed", "yes"), ("should", "stay"), + ("Proxy-Connection", "bad"), ("Transfer-Encoding", "also bad")]) + self.channel.writeAndFlush(HTTPClientRequestPart.head(requestHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":path", "/post"), (":method", "POST"), (":scheme", "https"), + (":authority", "example.org"), ("should", "stay")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .request) + } + + func testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForResponses() throws { + let writeRecorder = FramePayloadWriteRecorder() + XCTAssertNoThrow(try self.channel.pipeline.addHandler(writeRecorder).wait()) + XCTAssertNoThrow(try self.channel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).wait()) + + var responseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) + responseHead.headers = HTTPHeaders([("connection", "keep-alive, also-to-be-removed"), + ("keep-alive", "foo"), ("also-to-be-removed", "yes"), ("should", "stay"), + ("Proxy-Connection", "bad"), ("Transfer-Encoding", "also bad")]) + self.channel.writeAndFlush(HTTPServerResponsePart.head(responseHead), promise: nil) + + let expectedRequestHeaders = HPACKHeaders([(":status", "200"), ("should", "stay")]) + XCTAssertEqual(writeRecorder.flushedWrites.count, 1) + writeRecorder.flushedWrites[0].assertHeadersPayload(endStream: false, + headers: expectedRequestHeaders, + type: .response) + } +} diff --git a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift index a15f976c..01046756 100644 --- a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift +++ b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift @@ -65,12 +65,12 @@ final class FrameExpecter: ChannelInboundHandler { // A handler that keeps track of the writes made on a channel. Used to work around the limitations // in `EmbeddedChannel`. -final class FrameWriteRecorder: ChannelOutboundHandler { - typealias OutboundIn = HTTP2Frame - typealias OutboundOut = HTTP2Frame +final class WriteRecorder: ChannelOutboundHandler { + typealias OutboundIn = Write + typealias OutboundOut = Write - var flushedWrites: [HTTP2Frame] = [] - private var unflushedWrites: [HTTP2Frame] = [] + var flushedWrites: [Write] = [] + private var unflushedWrites: [Write] = [] func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { self.unflushedWrites.append(self.unwrapOutboundIn(data)) @@ -84,6 +84,9 @@ final class FrameWriteRecorder: ChannelOutboundHandler { } } +typealias FrameWriteRecorder = WriteRecorder +typealias FramePayloadWriteRecorder = WriteRecorder + /// A handler that keeps track of all reads made on a channel. final class InboundFrameRecorder: ChannelInboundHandler { diff --git a/Tests/NIOHTTP2Tests/TestUtilities.swift b/Tests/NIOHTTP2Tests/TestUtilities.swift index 760ed223..098870f3 100644 --- a/Tests/NIOHTTP2Tests/TestUtilities.swift +++ b/Tests/NIOHTTP2Tests/TestUtilities.swift @@ -264,37 +264,9 @@ extension HTTP2Frame { priority: HTTP2Frame.StreamPriorityData? = nil, type: HeadersType? = nil, file: StaticString = #file, line: UInt = #line) { - guard case .headers(let actualPayload) = self.payload else { - XCTFail("Expected HEADERS frame, got \(self.payload) instead", file: (file), line: line) - return - } - - XCTAssertEqual(actualPayload.endStream, endStream, - "Unexpected endStream: expected \(endStream), got \(actualPayload.endStream)", file: (file), line: line) XCTAssertEqual(self.streamID, streamID, "Unexpected streamID: expected \(streamID), got \(self.streamID)", file: (file), line: line) - XCTAssertEqual(headers, actualPayload.headers, "Non-equal headers: expected \(headers), got \(actualPayload.headers)", file: (file), line: line) - XCTAssertEqual(priority, actualPayload.priorityData, "Non-equal priorities: expected \(String(describing: priority)), got \(String(describing: actualPayload.priorityData))", file: (file), line: line) - - switch type { - case .some(.request): - XCTAssertNoThrow(try actualPayload.headers.validateRequestBlock(), - "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) - case .some(.response): - XCTAssertNoThrow(try actualPayload.headers.validateResponseBlock(), - "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) - case .some(.trailers): - XCTAssertNoThrow(try actualPayload.headers.validateTrailersBlock(), - "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) - case .some(.doNotValidate): - () // alright, let's not validate then - case .none: - XCTAssertTrue((try? actualPayload.headers.validateRequestBlock()) != nil || - (try? actualPayload.headers.validateResponseBlock()) != nil || - (try? actualPayload.headers.validateTrailersBlock()) != nil, - "\(actualPayload.headers) not a valid request/response/trailers header block", - file: (file), line: line) - } + self.payload.assertHeadersPayload(endStream: endStream, headers: headers, priority: priority, type: type, file: file, line: line) } /// Asserts that a given frame is a DATA frame matching this one. @@ -334,23 +306,9 @@ extension HTTP2Frame { /// Assert the given frame is a DATA frame with the appropriate settings. func assertDataFrame(endStream: Bool, streamID: HTTP2StreamID, payload: ByteBuffer, file: StaticString = #file, line: UInt = #line) { - guard case .data(let actualFrameBody) = self.payload else { - XCTFail("Expected DATA frame, got \(self.payload) instead", file: (file), line: line) - return - } - - guard case .byteBuffer(let actualPayload) = actualFrameBody.data else { - XCTFail("Expected ByteBuffer DATA frame, got \(actualFrameBody.data) instead", file: (file), line: line) - return - } - - XCTAssertEqual(actualFrameBody.endStream, endStream, - "Unexpected endStream: expected \(endStream), got \(actualFrameBody.endStream)", file: (file), line: line) XCTAssertEqual(self.streamID, streamID, "Unexpected streamID: expected \(streamID), got \(self.streamID)", file: (file), line: line) - XCTAssertEqual(actualPayload, payload, - "Unexpected body: expected \(payload), got \(actualPayload)", file: (file), line: line) - + self.payload.assertDataPayload(endStream: endStream, payload: payload, file: file, line: line) } func assertDataFrame(endStream: Bool, streamID: HTTP2StreamID, payload: FileRegion, file: StaticString = #file, line: UInt = #line) { @@ -532,6 +490,60 @@ extension HTTP2Frame { } } +extension HTTP2Frame.FramePayload { + func assertHeadersPayload(endStream: Bool, headers: HPACKHeaders, + priority: HTTP2Frame.StreamPriorityData? = nil, + type: HTTP2Frame.HeadersType? = nil, + file: StaticString = #file, line: UInt = #line) { + guard case .headers(let actualPayload) = self else { + XCTFail("Expected HEADERS payload, got \(self) instead", file: (file), line: line) + return + } + + XCTAssertEqual(actualPayload.endStream, endStream, + "Unexpected endStream: expected \(endStream), got \(actualPayload.endStream)", file: (file), line: line) + XCTAssertEqual(headers, actualPayload.headers, "Non-equal headers: expected \(headers), got \(actualPayload.headers)", file: (file), line: line) + XCTAssertEqual(priority, actualPayload.priorityData, "Non-equal priorities: expected \(String(describing: priority)), got \(String(describing: actualPayload.priorityData))", file: (file), line: line) + + switch type { + case .some(.request): + XCTAssertNoThrow(try actualPayload.headers.validateRequestBlock(), + "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) + case .some(.response): + XCTAssertNoThrow(try actualPayload.headers.validateResponseBlock(), + "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) + case .some(.trailers): + XCTAssertNoThrow(try actualPayload.headers.validateTrailersBlock(), + "\(actualPayload.headers) not a valid \(type!) headers block", file: (file), line: line) + case .some(.doNotValidate): + () // alright, let's not validate then + case .none: + XCTAssertTrue((try? actualPayload.headers.validateRequestBlock()) != nil || + (try? actualPayload.headers.validateResponseBlock()) != nil || + (try? actualPayload.headers.validateTrailersBlock()) != nil, + "\(actualPayload.headers) not a valid request/response/trailers header block", + file: (file), line: line) + } + } + + func assertDataPayload(endStream: Bool, payload: ByteBuffer, file: StaticString = #file, line: UInt = #line) { + guard case .data(let actualFrameBody) = self else { + XCTFail("Expected DATA payload, got \(self) instead", file: (file), line: line) + return + } + + guard case .byteBuffer(let actualPayload) = actualFrameBody.data else { + XCTFail("Expected ByteBuffer DATA payload, got \(actualFrameBody.data) instead", file: (file), line: line) + return + } + + XCTAssertEqual(actualFrameBody.endStream, endStream, + "Unexpected endStream: expected \(endStream), got \(actualFrameBody.endStream)", file: (file), line: line) + XCTAssertEqual(actualPayload, payload, + "Unexpected body: expected \(payload), got \(actualPayload)", file: (file), line: line) + } +} + extension Array where Element == HTTP2Frame { func assertFramesMatch(_ target: Candidate, dataFileRegionToByteBuffer: Bool = true, file: StaticString = #file, line: UInt = #line) where Candidate.Element == HTTP2Frame { guard self.count == target.count else {