1
0
mirror of https://github.com/apple/swift-nio-extras.git synced 2025-06-01 10:36:15 +08:00

Add HTTP types adapter for SwiftNIO

This commit is contained in:
Guoye Zhang 2023-06-20 14:34:01 -07:00
parent fb70a0f5e9
commit 2e8f942098
12 changed files with 1444 additions and 1 deletions

@ -1,4 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS]
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2]

@ -123,6 +123,34 @@ var targets: [PackageDescription.Target] = [
.product(name: "NIOEmbedded", package: "swift-nio"),
.product(name: "NIOTestUtils", package: "swift-nio"),
]),
.target(
name: "NIOHTTPTypes",
dependencies: [
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "NIOCore", package: "swift-nio"),
]),
.target(
name: "NIOHTTPTypesHTTP1",
dependencies: [
"NIOHTTPTypes",
.product(name: "NIOHTTP1", package: "swift-nio"),
]),
.target(
name: "NIOHTTPTypesHTTP2",
dependencies: [
"NIOHTTPTypes",
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
]),
.testTarget(
name: "NIOHTTPTypesHTTP1Tests",
dependencies: [
"NIOHTTPTypesHTTP1",
]),
.testTarget(
name: "NIOHTTPTypesHTTP2Tests",
dependencies: [
"NIOHTTPTypesHTTP2",
]),
]
let package = Package(
@ -131,10 +159,15 @@ let package = Package(
.library(name: "NIOExtras", targets: ["NIOExtras"]),
.library(name: "NIOSOCKS", targets: ["NIOSOCKS"]),
.library(name: "NIOHTTPCompression", targets: ["NIOHTTPCompression"]),
.library(name: "NIOHTTPTypes", targets: ["NIOHTTPTypes"]),
.library(name: "NIOHTTPTypesHTTP1", targets: ["NIOHTTPTypesHTTP1"]),
.library(name: "NIOHTTPTypesHTTP2", targets: ["NIOHTTPTypesHTTP2"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.27.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-http-types", from: "0.1.0"),
],
targets: targets
)

@ -50,3 +50,9 @@ On the [`nio-extras-0.1`](https://github.com/apple/swift-nio-extras/tree/nio-ext
- [`DebugInboundsEventHandler`](Sources/NIOExtras/DebugInboundEventsHandler.swift) Prints out all inbound events that travel through the `ChannelPipeline`.
- [`DebugOutboundsEventHandler`](Sources/NIOExtras/DebugOutboundEventsHandler.swift) Prints out all outbound events that travel through the `ChannelPipeline`.
- [`WritePCAPHandler`](Sources/NIOExtras/WritePCAPHandler.swift) A `ChannelHandler` that writes `.pcap` containing the traffic of the `ChannelPipeline` that you can inspect with Wireshark/tcpdump.
- [`HTTP1ToHTTPClientCodec`](Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/1 messages into shared HTTP types for the client side.
- [`HTTP1ToHTTPServerCodec`](Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/1 messages into shared HTTP types for the server side.
- [`HTTPToHTTP1ClientCodec`](Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift) A `ChannelHandler` that translates shared HTTP types into HTTP/1 messages for the client side for compatibility purposes.
- [`HTTPToHTTP1ServerCodec`](Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift) A `ChannelHandler` that translates shared HTTP types into HTTP/1 messages for the server side for compatibility purposes.
- [`HTTP2FramePayloadToHTTPClientCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the client side.
- [`HTTP2FramePayloadToHTTPServerCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the server side.

@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import HTTPTypes
/// The parts of a complete HTTP message, either request or response.
///
/// An HTTP message is made up of a request, or one or more response headers,
/// encoded by `.head`, zero or more body parts, and optionally some trailers. To
/// indicate that a complete HTTP message has been sent or received, we use `.end`,
/// which may also contain any trailers that make up the message.
public enum HTTPTypePart<HeadT: Equatable, BodyT: Equatable> {
case head(HeadT)
case body(BodyT)
case end(HTTPFields?)
}
extension HTTPTypePart: Sendable where HeadT: Sendable, BodyT: Sendable {}
extension HTTPTypePart: Equatable {}
/// The components of an HTTP request from the view of an HTTP client.
public typealias HTTPTypeClientRequestPart = HTTPTypePart<HTTPRequest, IOData>
/// The components of an HTTP request from the view of an HTTP server.
public typealias HTTPTypeServerRequestPart = HTTPTypePart<HTTPRequest, ByteBuffer>
/// The components of an HTTP response from the view of an HTTP client.
public typealias HTTPTypeClientResponsePart = HTTPTypePart<HTTPResponse, ByteBuffer>
/// The components of an HTTP response from the view of an HTTP server.
public typealias HTTPTypeServerResponsePart = HTTPTypePart<HTTPResponse, IOData>

@ -0,0 +1,110 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import NIOHTTP1
import NIOHTTPTypes
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types,
/// and vice versa, for use on the client side.
public final class HTTP1ToHTTPClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTPClientResponsePart
public typealias InboundOut = HTTPTypeClientResponsePart
public typealias OutboundIn = HTTPTypeClientRequestPart
public typealias OutboundOut = HTTPClientRequestPart
/// Initializes a `HTTP1ToHTTPClientCodec`.
public init() {
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch unwrapInboundIn(data) {
case .head(let head):
do {
context.fireChannelRead(wrapInboundOut(.head(try head.newResponse)))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(wrapInboundOut(.end(trailers?.newFields(splitCookie: false))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch unwrapOutboundIn(data) {
case .head(let request):
do {
context.write(wrapOutboundOut(.head(try HTTPRequestHead(request))), promise: promise)
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.write(wrapOutboundOut(.body(body)), promise: promise)
case .end(let trailers):
context.write(wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise)
}
}
}
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types,
/// and vice versa, for use on the server side.
public final class HTTP1ToHTTPServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTPServerRequestPart
public typealias InboundOut = HTTPTypeServerRequestPart
public typealias OutboundIn = HTTPTypeServerResponsePart
public typealias OutboundOut = HTTPServerResponsePart
private let secure: Bool
private let splitCookie: Bool
/// Initializes a `HTTP1ToHTTPServerCodec`.
/// - Parameters:
/// - secure: Whether "https" or "http" is used.
/// - splitCookie: Whether the cookies received from the server should be split
/// into multiple header fields. Defaults to false.
public init(secure: Bool, splitCookie: Bool = false) {
self.secure = secure
self.splitCookie = splitCookie
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch unwrapInboundIn(data) {
case .head(let head):
do {
context.fireChannelRead(wrapInboundOut(.head(try head.newRequest(secure: secure, splitCookie: splitCookie))))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(wrapInboundOut(.end(trailers?.newFields(splitCookie: false))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch unwrapOutboundIn(data) {
case .head(let response):
context.write(wrapOutboundOut(.head(HTTPResponseHead(response))), promise: promise)
case .body(let body):
context.write(wrapOutboundOut(.body(body)), promise: promise)
case .end(let trailers):
context.write(wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise)
}
}
}

@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import NIOHTTP1
import NIOHTTPTypes
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages,
/// and vice versa, for use on the client side.
///
/// This is intended for compatibility purposes where a channel handler working with
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types
/// abstraction.
public final class HTTPToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTPTypeClientResponsePart
public typealias InboundOut = HTTPClientResponsePart
public typealias OutboundIn = HTTPClientRequestPart
public typealias OutboundOut = HTTPTypeClientRequestPart
private let secure: Bool
private let splitCookie: Bool
/// Initializes a `HTTPToHTTP1ClientCodec`.
/// - Parameters:
/// - secure: Whether "https" or "http" is used.
/// - splitCookie: Whether the cookies sent by the client should be split
/// into multiple header fields. Defaults to true.
public init(secure: Bool, splitCookie: Bool = true) {
self.secure = secure
self.splitCookie = splitCookie
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch unwrapInboundIn(data) {
case .head(let head):
context.fireChannelRead(wrapInboundOut(.head(HTTPResponseHead(head))))
case .body(let body):
context.fireChannelRead(wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch unwrapOutboundIn(data) {
case .head(let request):
do {
context.write(wrapOutboundOut(.head(try request.newRequest(secure: secure, splitCookie: splitCookie))), promise: promise)
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.write(wrapOutboundOut(.body(body)), promise: promise)
case .end(let trailers):
context.write(wrapOutboundOut(.end(trailers?.newFields(splitCookie: false))), promise: promise)
}
}
}
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages,
/// and vice versa, for use on the server side.
///
/// This is intended for compatibility purposes where a channel handler working with
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types
/// abstraction.
public final class HTTPToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTPTypeServerRequestPart
public typealias InboundOut = HTTPServerRequestPart
public typealias OutboundIn = HTTPServerResponsePart
public typealias OutboundOut = HTTPTypeServerResponsePart
/// Initializes a `HTTPToHTTP1ServerCodec`.
public init() {
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch unwrapInboundIn(data) {
case .head(let head):
do {
context.fireChannelRead(wrapInboundOut(.head(try HTTPRequestHead(head))))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch unwrapOutboundIn(data) {
case .head(let response):
do {
context.write(wrapOutboundOut(.head(try response.newResponse)), promise: promise)
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.write(wrapOutboundOut(.body(body)), promise: promise)
case .end(let trailers):
context.write(wrapOutboundOut(.end(trailers?.newFields(splitCookie: false))), promise: promise)
}
}
}

@ -0,0 +1,195 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOHTTP1
import HTTPTypes
private enum HTTP1TypeConversionError: Error {
case invalidMethod
case missingPath
case invalidStatusCode
}
extension HTTPMethod {
init(_ newMethod: HTTPRequest.Method) {
switch newMethod {
case .get: self = .GET
case .put: self = .PUT
case .acl: self = .ACL
case .head: self = .HEAD
case .post: self = .POST
case .copy: self = .COPY
case .lock: self = .LOCK
case .move: self = .MOVE
case .bind: self = .BIND
case .link: self = .LINK
case .patch: self = .PATCH
case .trace: self = .TRACE
case .mkcol: self = .MKCOL
case .merge: self = .MERGE
case .purge: self = .PURGE
case .notify: self = .NOTIFY
case .search: self = .SEARCH
case .unlock: self = .UNLOCK
case .rebind: self = .REBIND
case .unbind: self = .UNBIND
case .report: self = .REPORT
case .delete: self = .DELETE
case .unlink: self = .UNLINK
case .connect: self = .CONNECT
case .msearch: self = .MSEARCH
case .options: self = .OPTIONS
case .propfind: self = .PROPFIND
case .checkout: self = .CHECKOUT
case .proppatch: self = .PROPPATCH
case .subscribe: self = .SUBSCRIBE
case .mkcalendar: self = .MKCALENDAR
case .mkactivity: self = .MKACTIVITY
case .unsubscribe: self = .UNSUBSCRIBE
case .source: self = .SOURCE
default: self = .RAW(value: newMethod.rawValue)
}
}
var newMethod: HTTPRequest.Method {
get throws {
switch self {
case .GET: return .get
case .PUT: return .put
case .ACL: return .acl
case .HEAD: return .head
case .POST: return .post
case .COPY: return .copy
case .LOCK: return .lock
case .MOVE: return .move
case .BIND: return .bind
case .LINK: return .link
case .PATCH: return .patch
case .TRACE: return .trace
case .MKCOL: return .mkcol
case .MERGE: return .merge
case .PURGE: return .purge
case .NOTIFY: return .notify
case .SEARCH: return .search
case .UNLOCK: return .unlock
case .REBIND: return .rebind
case .UNBIND: return .unbind
case .REPORT: return .report
case .DELETE: return .delete
case .UNLINK: return .unlink
case .CONNECT: return .connect
case .MSEARCH: return .msearch
case .OPTIONS: return .options
case .PROPFIND: return .propfind
case .CHECKOUT: return .checkout
case .PROPPATCH: return .proppatch
case .SUBSCRIBE: return .subscribe
case .MKCALENDAR: return .mkcalendar
case .MKACTIVITY: return .mkactivity
case .UNSUBSCRIBE: return .unsubscribe
case .SOURCE: return .source
case .RAW(value: let value):
guard let method = HTTPRequest.Method(value) else {
throw HTTP1TypeConversionError.invalidMethod
}
return method
}
}
}
}
extension HTTPHeaders {
init(_ newFields: HTTPFields) {
let fields = newFields.map { ($0.name.rawName, $0.value) }
self.init(fields)
}
func newFields(splitCookie: Bool) -> HTTPFields {
var fields = HTTPFields()
fields.reserveCapacity(count)
for (index, field) in enumerated() {
if index == 0 && field.name.lowercased() == "host" {
continue
}
if let name = HTTPField.Name(field.name) {
if splitCookie && name == .cookie, #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
fields.append(contentsOf: field.value.split(separator: "; ", omittingEmptySubsequences: false).map {
HTTPField(name: name, value: String($0))
})
} else {
fields.append(HTTPField(name: name, value: field.value))
}
}
}
return fields
}
}
extension HTTPRequestHead {
init(_ newRequest: HTTPRequest) throws {
guard let pathField = newRequest.pseudoHeaderFields.path else {
throw HTTP1TypeConversionError.missingPath
}
var headers = HTTPHeaders()
headers.reserveCapacity(newRequest.headerFields.count + 1)
if let authorityField = newRequest.pseudoHeaderFields.authority {
headers.add(name: "Host", value: authorityField.value)
}
var firstCookie = true
for field in newRequest.headerFields {
if field.name == .cookie {
if firstCookie {
firstCookie = false
headers.add(name: field.name.rawName, value: newRequest.headerFields[.cookie]!)
}
} else {
headers.add(name: field.name.rawName, value: field.value)
}
}
self.init(version: .http1_1,
method: HTTPMethod(newRequest.method),
uri: pathField.value,
headers: headers)
}
func newRequest(secure: Bool, splitCookie: Bool) throws -> HTTPRequest {
let method = try method.newMethod
let scheme = secure ? "https" : "http"
let authority = headers.first.flatMap { $0.name.lowercased() == "host" ? $0.value : nil }
return HTTPRequest(method: method,
scheme: scheme,
authority: authority,
path: uri,
headerFields: headers.newFields(splitCookie: splitCookie))
}
}
extension HTTPResponseHead {
init(_ newResponse: HTTPResponse) {
self.init(version: .http1_1,
status: HTTPResponseStatus(statusCode: newResponse.status.code,
reasonPhrase: newResponse.status.reasonPhrase),
headers: HTTPHeaders(newResponse.headerFields))
}
var newResponse: HTTPResponse {
get throws {
guard status.code <= 999 else {
throw HTTP1TypeConversionError.invalidStatusCode
}
let status = HTTPResponse.Status(code: Int(status.code), reasonPhrase: status.reasonPhrase)
return HTTPResponse(status: status, headerFields: headers.newFields(splitCookie: false))
}
}
}

@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOHTTP2
import NIOHPACK
extension HPACKHeaders {
/// Whether this `HTTPHeaders` corresponds to a final response or not.
///
/// This function is only valid if called on a response header block. If the :status header
/// is not present, this will throw.
fileprivate func isInformationalResponse() throws -> Bool {
return try self.peekPseudoHeader(name: ":status").first! == "1"
}
/// Grabs a pseudo-header from a header block. Does not remove it.
///
/// - Parameters:
/// - name: The header name to find.
/// - Returns: The value for this pseudo-header.
/// - Throws: `NIOHTTP2Errors` if there is no such header, or multiple.
internal func peekPseudoHeader(name: String) throws -> String {
// This could be done with .lazy.filter.map but that generates way more ARC traffic.
var headerValue: String? = nil
for (fieldName, fieldValue, _) in self {
if name == fieldName {
guard headerValue == nil else {
throw NIOHTTP2Errors.duplicatePseudoHeader(name)
}
headerValue = fieldValue
}
}
if let headerValue = headerValue {
return headerValue
} else {
throw NIOHTTP2Errors.missingPseudoHeader(name)
}
}
}
/// A state machine that keeps track of the header blocks sent or received and that determines the type of any
/// new header block.
struct HTTP2HeadersStateMachine {
/// The list of possible header frame types.
///
/// This is used in combination with introspection of the HTTP header blocks to determine what HTTP header block
/// a certain HTTP header is.
enum HeaderType {
/// A request header block.
case requestHead
/// An informational response header block. These can be sent zero or more times.
case informationalResponseHead
/// A final response header block.
case finalResponseHead
/// A trailer block. Once this is sent no further header blocks are acceptable.
case trailer
}
/// The previous header block.
private var previousHeader: HeaderType?
/// The mode of this connection: client or server.
private let mode: NIOHTTP2Handler.ParserMode
init(mode: NIOHTTP2Handler.ParserMode) {
self.mode = mode
}
/// Called when about to process an HTTP headers block to determine its type.
mutating func newHeaders(block: HPACKHeaders) throws -> HeaderType {
let newType: HeaderType
switch (self.mode, self.previousHeader) {
case (.server, .none):
// The first header block received on a server mode stream must be a request block.
newType = .requestHead
case (.client, .none),
(.client, .some(.informationalResponseHead)):
// The first header block received on a client mode stream may be either informational or final,
// depending on the value of the :status pseudo-header. Alternatively, if the previous
// header block was informational, the same possibilities apply.
newType = try block.isInformationalResponse() ? .informationalResponseHead : .finalResponseHead
case (.server, .some(.requestHead)),
(.client, .some(.finalResponseHead)):
// If the server has already received a request head, or the client has already received a final response,
// this is a trailer block.
newType = .trailer
case (.server, .some(.informationalResponseHead)),
(.server, .some(.finalResponseHead)),
(.client, .some(.requestHead)):
// These states should not be reachable!
preconditionFailure("Invalid internal state!")
case (.server, .some(.trailer)),
(.client, .some(.trailer)):
// TODO(cory): This should probably throw, as this can happen in malformed programs without the world ending.
preconditionFailure("Sending too many header blocks.")
}
self.previousHeader = newType
return newType
}
}

@ -0,0 +1,244 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import NIOHTTP2
import NIOHPACK
import HTTPTypes
import NIOHTTPTypes
// MARK: - Client
private struct BaseClientCodec {
private var headerStateMachine: HTTP2HeadersStateMachine = HTTP2HeadersStateMachine(mode: .client)
private var outgoingHTTP1RequestHead: HTTPRequest?
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPTypeClientResponsePart?, second: HTTPTypeClientResponsePart?) {
switch data {
case .headers(let headerContent):
switch try self.headerStateMachine.newHeaders(block: headerContent.headers) {
case .trailer:
return (first: .end(try headerContent.headers.newTrailers), second: nil)
case .informationalResponseHead:
return (first: .head(try headerContent.headers.newResponse), second: nil)
case .finalResponseHead:
guard outgoingHTTP1RequestHead != nil else {
preconditionFailure("Expected not to get a response without having sent a request")
}
self.outgoingHTTP1RequestHead = nil
let respHead = try headerContent.headers.newResponse
let first = HTTPTypeClientResponsePart.head(respHead)
var second: HTTPTypeClientResponsePart? = nil
if headerContent.endStream {
second = .end(nil)
}
return (first: first, second: second)
case .requestHead:
preconditionFailure("A client can not receive request heads")
}
case .data(let content):
guard case .byteBuffer(let b) = content.data else {
preconditionFailure("Received DATA frame with non-bytebuffer IOData")
}
var first = HTTPTypeClientResponsePart.body(b)
var second: HTTPTypeClientResponsePart? = nil
if content.endStream {
if b.readableBytes == 0 {
first = .end(nil)
} else {
second = .end(nil)
}
}
return (first: first, second: second)
case .alternativeService, .rstStream, .priority, .windowUpdate, .settings, .pushPromise, .ping, .goAway, .origin:
// These are not meaningful in HTTP messaging, so drop them.
return (first: nil, second: nil)
}
}
mutating func processOutboundData(_ data: HTTPTypeClientRequestPart, allocator: ByteBufferAllocator) throws -> HTTP2Frame.FramePayload {
switch data {
case .head(let head):
precondition(self.outgoingHTTP1RequestHead == nil, "Only a single HTTP request allowed per HTTP2 stream")
self.outgoingHTTP1RequestHead = head
let headerContent = HTTP2Frame.FramePayload.Headers(headers: HPACKHeaders(head))
return .headers(headerContent)
case .body(let body):
return .data(HTTP2Frame.FramePayload.Data(data: body))
case .end(let trailers):
if let trailers = trailers {
return .headers(.init(headers: HPACKHeaders(trailers),
endStream: true))
} else {
return .data(.init(data: .byteBuffer(allocator.buffer(capacity: 0)), endStream: true))
}
}
}
}
/// A simple channel handler that translates HTTP/2 concepts into shared HTTP types,
/// and vice versa, for use on the client side.
///
/// Use this channel handler alongside the `HTTP2StreamMultiplexer` to
/// help provide an HTTP transaction-level abstraction on top of an HTTP/2 multiplexed
/// connection.
///
/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type.
public final class HTTP2FramePayloadToHTTPClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPTypeClientResponsePart
public typealias OutboundIn = HTTPTypeClientRequestPart
public typealias OutboundOut = HTTP2Frame.FramePayload
private var baseCodec: BaseClientCodec = .init()
public init() {
}
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<Void>?) {
let requestPart = self.unwrapOutboundIn(data)
do {
let transformedPayload = try self.baseCodec.processOutboundData(requestPart, allocator: context.channel.allocator)
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
} catch {
promise?.fail(error)
context.fireErrorCaught(error)
}
}
}
// MARK: - Server
private struct BaseServerCodec {
private var headerStateMachine: HTTP2HeadersStateMachine = HTTP2HeadersStateMachine(mode: .server)
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPTypeServerRequestPart?, second: HTTPTypeServerRequestPart?) {
switch data {
case .headers(let headerContent):
if case .trailer = try self.headerStateMachine.newHeaders(block: headerContent.headers) {
return (first: .end(try headerContent.headers.newTrailers), second: nil)
} else {
let reqHead = try headerContent.headers.newRequest
let first = HTTPTypeServerRequestPart.head(reqHead)
var second: HTTPTypeServerRequestPart? = nil
if headerContent.endStream {
second = .end(nil)
}
return (first: first, second: second)
}
case .data(let dataContent):
guard case .byteBuffer(let b) = dataContent.data else {
preconditionFailure("Received non-byteBuffer IOData from network")
}
var first = HTTPTypeServerRequestPart.body(b)
var second: HTTPTypeServerRequestPart? = nil
if dataContent.endStream {
if b.readableBytes == 0 {
first = .end(nil)
} else {
second = .end(nil)
}
}
return (first: first, second: second)
default:
// Any other frame type is ignored.
return (first: nil, second: nil)
}
}
mutating func processOutboundData(_ data: HTTPTypeServerResponsePart, allocator: ByteBufferAllocator) -> HTTP2Frame.FramePayload {
switch data {
case .head(let head):
let payload = HTTP2Frame.FramePayload.Headers(headers: HPACKHeaders(head))
return .headers(payload)
case .body(let body):
let payload = HTTP2Frame.FramePayload.Data(data: body)
return .data(payload)
case .end(let trailers):
if let trailers = trailers {
return .headers(.init(headers: HPACKHeaders(trailers),
endStream: true))
} else {
return .data(.init(data: .byteBuffer(allocator.buffer(capacity: 0)), endStream: true))
}
}
}
}
/// A simple channel handler that translates HTTP/2 concepts into shared HTTP types,
/// and vice versa, for use on the server side.
///
/// Use this channel handler alongside the `HTTP2StreamMultiplexer` to
/// help provide an HTTP transaction-level abstraction on top of an HTTP/2 multiplexed
/// connection.
///
/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type.
public final class HTTP2FramePayloadToHTTPServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPTypeServerRequestPart
public typealias OutboundIn = HTTPTypeServerResponsePart
public typealias OutboundOut = HTTP2Frame.FramePayload
private var baseCodec: BaseServerCodec = .init()
public init() {
}
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<Void>?) {
let responsePart = self.unwrapOutboundIn(data)
let transformedPayload = self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator)
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
}
}

@ -0,0 +1,263 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOHPACK
import HTTPTypes
private enum HTTP2TypeConversionError: Error {
case multipleMethod
case multipleScheme
case multipleAuthority
case multiplePath
case multipleProtocol
case missingMethod
case invalidMethod
case multipleStatus
case missingStatus
case invalidStatus
case pseudoFieldNotFirst
case pseudoFieldInTrailers
}
extension HPACKIndexing {
init(_ newIndexingStrategy: HTTPField.DynamicTableIndexingStrategy) {
switch newIndexingStrategy {
case .avoid: self = .nonIndexable
case .disallow: self = .neverIndexed
default: self = .indexable
}
}
var newIndexingStrategy: HTTPField.DynamicTableIndexingStrategy {
switch self {
case .indexable: return .automatic
case .nonIndexable: return .avoid
case .neverIndexed: return .disallow
}
}
}
extension HPACKHeaders {
mutating func add(newField field: HTTPField) {
add(name: field.name.canonicalName, value: field.value, indexing: HPACKIndexing(field.indexingStrategy))
}
init(_ newRequest: HTTPRequest) {
var headers = HPACKHeaders()
headers.reserveCapacity(newRequest.headerFields.count + 5)
headers.add(newField: newRequest.pseudoHeaderFields.method)
if let field = newRequest.pseudoHeaderFields.scheme {
headers.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.authority {
headers.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.path {
headers.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.extendedConnectProtocol {
headers.add(newField: field)
}
for field in newRequest.headerFields {
headers.add(newField: field)
}
self = headers
}
init(_ newResponse: HTTPResponse) {
var headers = HPACKHeaders()
headers.reserveCapacity(newResponse.headerFields.count + 1)
headers.add(newField: newResponse.pseudoHeaderFields.status)
for field in newResponse.headerFields {
headers.add(newField: field)
}
self = headers
}
init(_ newTrailers: HTTPFields) {
var headers = HPACKHeaders()
headers.reserveCapacity(newTrailers.count)
for field in newTrailers {
headers.add(newField: field)
}
self = headers
}
var newRequest: HTTPRequest {
get throws {
var methodString: String? = nil
var methodIndexable: HPACKIndexing = .indexable
var schemeString: String? = nil
var schemeIndexable: HPACKIndexing = .indexable
var authorityString: String? = nil
var authorityIndexable: HPACKIndexing = .indexable
var pathString: String? = nil
var pathIndexable: HPACKIndexing = .indexable
var protocolString: String? = nil
var protocolIndexable: HPACKIndexing = .indexable
var i = startIndex
while i != endIndex {
let (name, value, indexable) = self[i]
if !name.hasPrefix(":") {
break
}
switch name {
case ":method":
if methodString != nil {
throw HTTP2TypeConversionError.multipleMethod
}
methodString = value
methodIndexable = indexable
case ":scheme":
if schemeString != nil {
throw HTTP2TypeConversionError.multipleScheme
}
schemeString = value
schemeIndexable = indexable
case ":authority":
if authorityString != nil {
throw HTTP2TypeConversionError.multipleAuthority
}
authorityString = value
authorityIndexable = indexable
case ":path":
if pathString != nil {
throw HTTP2TypeConversionError.multiplePath
}
pathString = value
pathIndexable = indexable
case ":protocol":
if protocolString != nil {
throw HTTP2TypeConversionError.multipleProtocol
}
protocolString = value
protocolIndexable = indexable
default:
continue
}
i = index(after: i)
}
guard let methodString else {
throw HTTP2TypeConversionError.missingMethod
}
guard let method = HTTPRequest.Method(methodString) else {
throw HTTP2TypeConversionError.invalidMethod
}
var request = HTTPRequest(method: method,
scheme: schemeString,
authority: authorityString,
path: pathString)
request.pseudoHeaderFields.method.indexingStrategy = methodIndexable.newIndexingStrategy
request.pseudoHeaderFields.scheme?.indexingStrategy = schemeIndexable.newIndexingStrategy
request.pseudoHeaderFields.authority?.indexingStrategy = authorityIndexable.newIndexingStrategy
request.pseudoHeaderFields.path?.indexingStrategy = pathIndexable.newIndexingStrategy
if let protocolString {
request.extendedConnectProtocol = protocolString
request.pseudoHeaderFields.extendedConnectProtocol?.indexingStrategy = protocolIndexable.newIndexingStrategy
}
request.headerFields.reserveCapacity(count)
while i != endIndex {
let (name, value, indexable) = self[i]
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldNotFirst
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = indexable.newIndexingStrategy
request.headerFields.append(field)
}
i = index(after: i)
}
return request
}
}
var newResponse: HTTPResponse {
get throws {
var statusString: String? = nil
var statusIndexable: HPACKIndexing = .indexable
var i = startIndex
while i != endIndex {
let (name, value, indexable) = self[i]
if !name.hasPrefix(":") {
break
}
switch name {
case ":status":
if statusString != nil {
throw HTTP2TypeConversionError.multipleStatus
}
statusString = value
statusIndexable = indexable
default:
continue
}
i = index(after: i)
}
guard let statusString else {
throw HTTP2TypeConversionError.missingStatus
}
guard let status = Int(statusString),
(0...999).contains(status) else {
throw HTTP2TypeConversionError.invalidStatus
}
var response = HTTPResponse(status: HTTPResponse.Status(code: status))
response.pseudoHeaderFields.status.indexingStrategy = statusIndexable.newIndexingStrategy
response.headerFields.reserveCapacity(count)
while i != endIndex {
let (name, value, indexable) = self[i]
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldNotFirst
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = indexable.newIndexingStrategy
response.headerFields.append(field)
}
i = index(after: i)
}
return response
}
}
var newTrailers: HTTPFields {
get throws {
var fields = HTTPFields()
fields.reserveCapacity(count)
for (name, value, indexable) in self {
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldInTrailers
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = indexable.newIndexingStrategy
fields.append(field)
}
}
return fields
}
}
}

@ -0,0 +1,170 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import NIOEmbedded
import NIOHTTP1
import HTTPTypes
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
/// A handler that keeps track of all reads made on a channel.
private final class InboundRecorder<Frame>: ChannelInboundHandler {
typealias InboundIn = Frame
var receivedFrames: [Frame] = []
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedFrames.append(self.unwrapInboundIn(data))
}
}
private extension HTTPField.Name {
static let xFoo = Self("X-Foo")!
}
final class NIOHTTPTypesHTTP1Tests: XCTestCase {
var channel: EmbeddedChannel!
override func setUp() {
super.setUp()
self.channel = EmbeddedChannel()
}
override func tearDown() {
self.channel = nil
super.tearDown()
}
static let request = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
.accept: "*/*",
.acceptEncoding: "gzip",
.acceptEncoding: "br",
.cookie: "a=b",
.cookie: "c=d",
.trailer: "X-Foo",
])
static let requestNoSplitCookie = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
.accept: "*/*",
.acceptEncoding: "gzip",
.acceptEncoding: "br",
.cookie: "a=b; c=d",
.trailer: "X-Foo",
])
static let oldRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: [
"Host": "www.example.com",
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Accept-Encoding": "br",
"Cookie": "a=b; c=d",
"Trailer": "X-Foo",
])
static let response = HTTPResponse(status: .ok, headerFields: [
.server: "HTTPServer/1.0",
.trailer: "X-Foo",
])
static let oldResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: [
"Server": "HTTPServer/1.0",
"Trailer": "X-Foo",
])
static let trailers: HTTPFields = [.xFoo: "Bar"]
static let oldTrailers: HTTPHeaders = ["X-Foo": "Bar"]
func testClientHTTP1ToHTTP() throws {
let recorder = InboundRecorder<HTTPTypeClientResponsePart>()
try self.channel.pipeline.addHandlers(HTTP1ToHTTPClientCodec(), recorder).wait()
try self.channel.writeOutbound(HTTPTypeClientRequestPart.head(Self.request))
try self.channel.writeOutbound(HTTPTypeClientRequestPart.end(Self.trailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPClientRequestPart.self), .head(Self.oldRequest))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPClientRequestPart.self), .end(Self.oldTrailers))
try self.channel.writeInbound(HTTPClientResponsePart.head(Self.oldResponse))
try self.channel.writeInbound(HTTPClientResponsePart.end(Self.oldTrailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.response))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
XCTAssertTrue(try channel.finish().isClean)
}
func testServerHTTP1ToHTTP() throws {
let recorder = InboundRecorder<HTTPTypeServerRequestPart>()
try self.channel.pipeline.addHandlers(HTTP1ToHTTPServerCodec(secure: true), recorder).wait()
try self.channel.writeInbound(HTTPServerRequestPart.head(Self.oldRequest))
try self.channel.writeInbound(HTTPServerRequestPart.end(Self.oldTrailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.requestNoSplitCookie))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
try self.channel.writeOutbound(HTTPTypeServerResponsePart.head(Self.response))
try self.channel.writeOutbound(HTTPTypeServerResponsePart.end(Self.trailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPServerResponsePart.self), .head(Self.oldResponse))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPServerResponsePart.self), .end(Self.oldTrailers))
XCTAssertTrue(try channel.finish().isClean)
}
func testClientHTTPToHTTP1() throws {
let recorder = InboundRecorder<HTTPClientResponsePart>()
try self.channel.pipeline.addHandlers(HTTPToHTTP1ClientCodec(secure: true), recorder).wait()
try self.channel.writeOutbound(HTTPClientRequestPart.head(Self.oldRequest))
try self.channel.writeOutbound(HTTPClientRequestPart.end(Self.oldTrailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeClientRequestPart.self), .head(Self.request))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeClientRequestPart.self), .end(Self.trailers))
try self.channel.writeInbound(HTTPTypeClientResponsePart.head(Self.response))
try self.channel.writeInbound(HTTPTypeClientResponsePart.end(Self.trailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.oldResponse))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.oldTrailers))
XCTAssertTrue(try channel.finish().isClean)
}
func testServerHTTPToHTTP1() throws {
let recorder = InboundRecorder<HTTPServerRequestPart>()
try self.channel.pipeline.addHandlers(HTTPToHTTP1ServerCodec(), recorder).wait()
try self.channel.writeInbound(HTTPTypeServerRequestPart.head(Self.request))
try self.channel.writeInbound(HTTPTypeServerRequestPart.end(Self.trailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.oldRequest))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.oldTrailers))
try self.channel.writeOutbound(HTTPServerResponsePart.head(Self.oldResponse))
try self.channel.writeOutbound(HTTPServerResponsePart.end(Self.oldTrailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeServerResponsePart.self), .head(Self.response))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeServerResponsePart.self), .end(Self.trailers))
XCTAssertTrue(try channel.finish().isClean)
}
}

@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 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 NIOCore
import NIOEmbedded
import NIOHTTP2
import NIOHPACK
import HTTPTypes
import NIOHTTPTypes
import NIOHTTPTypesHTTP2
/// A handler that keeps track of all reads made on a channel.
private final class InboundRecorder<Frame>: ChannelInboundHandler {
typealias InboundIn = Frame
var receivedFrames: [Frame] = []
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedFrames.append(self.unwrapInboundIn(data))
}
}
private extension HTTPField.Name {
static let xFoo = Self("X-Foo")!
}
private extension HTTP2Frame.FramePayload {
var headers: HPACKHeaders? {
if case .headers(let headers) = self {
return headers.headers
} else {
return nil
}
}
init(headers: HPACKHeaders) {
self = .headers(.init(headers: headers))
}
}
final class NIOHTTPTypesHTTP2Tests: XCTestCase {
var channel: EmbeddedChannel!
override func setUp() {
super.setUp()
self.channel = EmbeddedChannel()
}
override func tearDown() {
self.channel = nil
super.tearDown()
}
static let request = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
.accept: "*/*",
.acceptEncoding: "gzip",
.acceptEncoding: "br",
.trailer: "X-Foo",
.cookie: "a=b",
.cookie: "c=d",
])
static let oldRequest: HPACKHeaders = [
":method": "GET",
":scheme": "https",
":authority": "www.example.com",
":path": "/",
"accept": "*/*",
"accept-encoding": "gzip",
"accept-encoding": "br",
"trailer": "X-Foo",
"cookie": "a=b",
"cookie": "c=d",
]
static let response = HTTPResponse(status: .ok, headerFields: [
.server: "HTTPServer/1.0",
.trailer: "X-Foo",
])
static let oldResponse: HPACKHeaders = [
":status": "200",
"server": "HTTPServer/1.0",
"trailer": "X-Foo",
]
static let trailers: HTTPFields = [.xFoo: "Bar"]
static let oldTrailers: HPACKHeaders = ["x-foo": "Bar"]
func testClientHTTP2ToHTTP() throws {
let recorder = InboundRecorder<HTTPTypeClientResponsePart>()
try self.channel.pipeline.addHandlers(HTTP2FramePayloadToHTTPClientCodec(), recorder).wait()
try self.channel.writeOutbound(HTTPTypeClientRequestPart.head(Self.request))
try self.channel.writeOutbound(HTTPTypeClientRequestPart.end(Self.trailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldRequest)
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldTrailers)
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldResponse))
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldTrailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.response))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
XCTAssertTrue(try channel.finish().isClean)
}
func testServerHTTP2ToHTTP() throws {
let recorder = InboundRecorder<HTTPTypeServerRequestPart>()
try self.channel.pipeline.addHandlers(HTTP2FramePayloadToHTTPServerCodec(), recorder).wait()
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldRequest))
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldTrailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.request))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
try self.channel.writeOutbound(HTTPTypeServerResponsePart.head(Self.response))
try self.channel.writeOutbound(HTTPTypeServerResponsePart.end(Self.trailers))
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldResponse)
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldTrailers)
XCTAssertTrue(try channel.finish().isClean)
}
}