Merge branch 'main' into dn-automatic-compression-format-detection

This commit is contained in:
Cory Benfield 2024-03-26 09:52:46 +00:00 committed by GitHub
commit 43bf9ef378
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1679 additions and 67 deletions

View File

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

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:5.8
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
@ -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: "1.0.0"),
],
targets: targets
)

View File

@ -14,7 +14,7 @@ All code will go through code review like in the other repositories related to t
`swift-nio-extras` part of the SwiftNIO 2 family of repositories and depends on the following:
- [`swift-nio`](https://github.com/apple/swift-nio), version 2.30.0 or better.
- Swift 5.7
- Swift 5.7.1
- `zlib` and its development headers installed on the system. But don't worry, you'll find `zlib` on pretty much any UNIX system that can compile any sort of code.
To depend on `swift-nio-extras`, put the following in the `dependencies` of your `Package.swift`:
@ -25,7 +25,7 @@ To depend on `swift-nio-extras`, put the following in the `dependencies` of your
### Support for older Swift versions
The most recent versions of SwiftNIO Extras support Swift 5.7 and newer. The minimum Swift version supported by SwiftNIO Extras releases are detailed below:
The most recent versions of SwiftNIO Extras support Swift 5.7.1 and newer. The minimum Swift version supported by SwiftNIO Extras releases are detailed below:
SwiftNIO Extras | Minimum Swift Version
--------------------|----------------------
@ -34,7 +34,8 @@ SwiftNIO Extras | Minimum Swift Version
`1.11.0 ..< 1.14.0` | 5.4
`1.14.0 ..< 1.19.0` | 5.5.2
`1.19.0 ..< 1.20.0` | 5.6
`1.20.0 ...` | 5.7
`1.20.0 ..< 1.23.0` | 5.7.1
`1.23.0 ...` | 5.8
On the [`nio-extras-0.1`](https://github.com/apple/swift-nio-extras/tree/nio-extras-0.1) branch, you can find the `swift-nio-extras` version for the SwiftNIO 1 family. It requires Swift 4.1 or better.
@ -51,3 +52,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.

View File

@ -59,45 +59,54 @@ private final class HTTPHandler: ChannelInboundHandler {
}
}
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let quiesce = ServerQuiescingHelper(group: group)
private func runServer() throws {
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let signalQueue = DispatchQueue(label: "io.swift-nio.NIOExtrasDemo.SignalHandlingQueue")
let signalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue)
let fullyShutdownPromise: EventLoopPromise<Void> = group.next().makePromise()
signalSource.setEventHandler {
signalSource.cancel()
print("\nreceived signal, initiating shutdown which should complete after the last request finished.")
do {
// This nested block is necessary to ensure that all the destructors for objects defined inside are called before the final call to group.syncShutdownGracefully(). A possible side effect of not doing this is a run-time error "Cannot schedule tasks on an EventLoop that has already shut down".
let quiesce = ServerQuiescingHelper(group: group)
quiesce.initiateShutdown(promise: fullyShutdownPromise)
}
signal(SIGINT, SIG_IGN)
signalSource.resume()
let signalQueue = DispatchQueue(label: "io.swift-nio.NIOExtrasDemo.SignalHandlingQueue")
let signalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue)
let fullyShutdownPromise: EventLoopPromise<Void> = group.next().makePromise()
signalSource.setEventHandler {
signalSource.cancel()
print("\nreceived signal, initiating shutdown which should complete after the last request finished.")
do {
let serverChannel = try ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.serverChannelInitializer { channel in
channel.pipeline.addHandler(quiesce.makeServerChannelHandler(channel: channel))
quiesce.initiateShutdown(promise: fullyShutdownPromise)
}
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap {
channel.pipeline.addHandler(HTTPHandler())
}
signal(SIGINT, SIG_IGN)
signalSource.resume()
do {
let serverChannel = try ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.serverChannelInitializer { channel in
channel.pipeline.addHandler(quiesce.makeServerChannelHandler(channel: channel))
}
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap {
channel.pipeline.addHandler(HTTPHandler())
}
}
.bind(host: "localhost", port: 0)
.wait()
print("HTTP server up and running on \(serverChannel.localAddress!)")
print("to connect to this server, run")
print(" curl http://localhost:\(serverChannel.localAddress!.port!)")
} catch {
try group.syncShutdownGracefully()
throw error
}
.bind(host: "localhost", port: 0)
.wait()
print("HTTP server up and running on \(serverChannel.localAddress!)")
print("to connect to this server, run")
print(" curl http://localhost:\(serverChannel.localAddress!.port!)")
} catch {
try fullyShutdownPromise.futureResult.wait()
}
try group.syncShutdownGracefully()
throw error
}
try fullyShutdownPromise.futureResult.wait()
try group.syncShutdownGracefully()
try runServer()

View File

@ -13,6 +13,8 @@
//===----------------------------------------------------------------------===//
#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#else
import Glibc
#endif

View File

@ -14,6 +14,8 @@
#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#else
import Glibc
#endif

View File

@ -30,7 +30,7 @@ import NIOCore
/// | ABC | DEF | GHI |
/// +-----+-----+-----+
///
public class LineBasedFrameDecoder: ByteToMessageDecoder {
public class LineBasedFrameDecoder: ByteToMessageDecoder & NIOSingleStepByteToMessageDecoder {
/// `ByteBuffer` is the expected type passed in.
public typealias InboundIn = ByteBuffer
/// `ByteBuffer`s will be passed to the next stage.
@ -57,6 +57,14 @@ public class LineBasedFrameDecoder: ByteToMessageDecoder {
}
}
/// Decode data in the supplied buffer.
/// - Parameters:
/// - buffer: Buffer containing data to decode.
/// - Returns: The decoded object or `nil` if we require more bytes.
public func decode(buffer: inout NIOCore.ByteBuffer) throws -> NIOCore.ByteBuffer? {
return try self.findNextFrame(buffer: &buffer)
}
/// Decode all remaining data.
/// If it is not possible to consume all the data then ``NIOExtrasErrors/LeftOverBytesError`` is reported via `context.fireErrorCaught`
/// - Parameters:
@ -72,6 +80,20 @@ public class LineBasedFrameDecoder: ByteToMessageDecoder {
return .needMoreData
}
/// Decode all remaining data.
/// If it is not possible to consume all the data then ``NIOExtrasErrors/LeftOverBytesError`` is reported via `context.fireErrorCaught`
/// - Parameters:
/// - buffer: Buffer containing the data to decode.
/// - seenEOF: Has end of file been seen.
/// - Returns: The decoded object or `nil` if we require more bytes.
public func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> InboundOut? {
let decoded = try self.decode(buffer: &buffer)
if buffer.readableBytes > 0 {
throw NIOExtrasErrors.LeftOverBytesError(leftOverBytes: buffer)
}
return decoded
}
private func findNextFrame(buffer: inout ByteBuffer) throws -> ByteBuffer? {
let view = buffer.readableBytesView.dropFirst(self.lastScanOffset)
// look for the delimiter

View File

@ -35,7 +35,8 @@ public class NIOPCAPRingBuffer {
self.maximumFragments = maximumFragments
self.maximumBytes = maximumBytes
self.pcapCurrentBytes = 0
self.pcapFragments = CircularBuffer(initialCapacity: maximumFragments)
// Don't default to `maximumFragments` as it will be `.max` on some paths.
self.pcapFragments = CircularBuffer()
}
/// Initialise the buffer, setting constraints
@ -88,7 +89,7 @@ public class NIOPCAPRingBuffer {
}
/// Emit the captured data to a consuming function; then clear the captured data.
/// - Returns: A ciruclar buffer of captured fragments.
/// - Returns: A circular buffer of captured fragments.
public func emitPCAP() -> CircularBuffer<ByteBuffer> {
let toReturn = self.pcapFragments // Copy before clearing.
self.pcapFragments.removeAll(keepingCapacity: true)

View File

@ -14,6 +14,8 @@
#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#else
import Glibc
#endif

View File

@ -152,6 +152,7 @@ public enum NIOHTTPDecompression {
self.stream.zalloc = nil
self.stream.zfree = nil
self.stream.opaque = nil
self.inflated = 0
let rc = CNIOExtrasZlib_inflateInit2(&self.stream, Self.windowBitsWithAutomaticCompressionFormatDetection)
guard rc == Z_OK else {

View File

@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// 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 HTTPTypes
import NIOCore
/// The parts of a complete HTTP request.
///
/// An HTTP request message is made up of a request 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 HTTPRequestPart: Sendable, Hashable {
case head(HTTPRequest)
case body(ByteBuffer)
case end(HTTPFields?)
}
/// The parts of a complete HTTP response.
///
/// An HTTP response message is made up of 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 HTTPResponsePart: Sendable, Hashable {
case head(HTTPResponse)
case body(ByteBuffer)
case end(HTTPFields?)
}

View File

@ -0,0 +1,117 @@
//===----------------------------------------------------------------------===//
//
// 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 HTTPTypes
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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTPClientResponsePart
public typealias InboundOut = HTTPResponsePart
public typealias OutboundIn = HTTPRequestPart
public typealias OutboundOut = HTTPClientRequestPart
/// Initializes a `HTTP1ToHTTPClientCodec`.
public init() {}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head(let head):
do {
let newResponse = try HTTPResponse(head)
context.fireChannelRead(self.wrapInboundOut(.head(newResponse)))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(self.wrapInboundOut(.body(body)))
case .end(let trailers):
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) }
context.fireChannelRead(self.wrapInboundOut(.end(newTrailers)))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch self.unwrapOutboundIn(data) {
case .head(let request):
do {
let oldRequest = try HTTPRequestHead(request)
context.write(self.wrapOutboundOut(.head(oldRequest)), promise: promise)
} catch {
context.fireErrorCaught(error)
promise?.fail(error)
}
case .body(let body):
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: promise)
case .end(let trailers):
context.write(self.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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTPServerRequestPart
public typealias InboundOut = HTTPRequestPart
public typealias OutboundIn = HTTPResponsePart
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 self.unwrapInboundIn(data) {
case .head(let head):
do {
let newRequest = try HTTPRequest(head, secure: self.secure, splitCookie: self.splitCookie)
context.fireChannelRead(self.wrapInboundOut(.head(newRequest)))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(self.wrapInboundOut(.body(body)))
case .end(let trailers):
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) }
context.fireChannelRead(self.wrapInboundOut(.end(newTrailers)))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch self.unwrapOutboundIn(data) {
case .head(let response):
let oldResponse = HTTPResponseHead(response)
context.write(self.wrapOutboundOut(.head(oldResponse)), promise: promise)
case .body(let body):
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: promise)
case .end(let trailers):
context.write(self.wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise)
}
}
}

View File

@ -0,0 +1,134 @@
//===----------------------------------------------------------------------===//
//
// 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 HTTPTypes
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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTPResponsePart
public typealias InboundOut = HTTPClientResponsePart
public typealias OutboundIn = HTTPClientRequestPart
public typealias OutboundOut = HTTPRequestPart
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. Splitting the `Cookie`
/// header field improves the performance of HTTP/2 and
/// HTTP/3 clients by allowing individual cookies to be
/// indexed separately in the dynamic table. It has no
/// effects in HTTP/1. Defaults to true.
public init(secure: Bool, splitCookie: Bool = true) {
self.secure = secure
self.splitCookie = splitCookie
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head(let head):
let oldResponse = HTTPResponseHead(head)
context.fireChannelRead(self.wrapInboundOut(.head(oldResponse)))
case .body(let body):
context.fireChannelRead(self.wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(self.wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch self.unwrapOutboundIn(data) {
case .head(let request):
do {
let newRequest = try HTTPRequest(request, secure: self.secure, splitCookie: self.splitCookie)
context.write(self.wrapOutboundOut(.head(newRequest)), promise: promise)
} catch {
context.fireErrorCaught(error)
promise?.fail(error)
}
case .body(.byteBuffer(let body)):
context.write(self.wrapOutboundOut(.body(body)), promise: promise)
case .body:
fatalError("File region not supported")
case .end(let trailers):
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) }
context.write(self.wrapOutboundOut(.end(newTrailers)), 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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTPRequestPart
public typealias InboundOut = HTTPServerRequestPart
public typealias OutboundIn = HTTPServerResponsePart
public typealias OutboundOut = HTTPResponsePart
/// Initializes a `HTTPToHTTP1ServerCodec`.
public init() {}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head(let head):
do {
let oldRequest = try HTTPRequestHead(head)
context.fireChannelRead(self.wrapInboundOut(.head(oldRequest)))
} catch {
context.fireErrorCaught(error)
}
case .body(let body):
context.fireChannelRead(self.wrapInboundOut(.body(body)))
case .end(let trailers):
context.fireChannelRead(self.wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch self.unwrapOutboundIn(data) {
case .head(let response):
do {
let newResponse = try HTTPResponse(response)
context.write(self.wrapOutboundOut(.head(newResponse)), promise: promise)
} catch {
context.fireErrorCaught(error)
promise?.fail(error)
}
case .body(.byteBuffer(let body)):
context.write(self.wrapOutboundOut(.body(body)), promise: promise)
case .body:
fatalError("File region not supported")
case .end(let trailers):
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) }
context.write(self.wrapOutboundOut(.end(newTrailers)), promise: promise)
}
}
}

View File

@ -0,0 +1,225 @@
//===----------------------------------------------------------------------===//
//
// 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 HTTPTypes
import NIOHTTP1
public struct HTTP1TypeConversionError: Error, Equatable {
private enum Internal {
case invalidMethod
case missingPath
case invalidStatusCode
}
private let value: Internal
private init(_ value: Internal) {
self.value = value
}
/// Failed to create HTTPRequest.Method from HTTPMethod
public static var invalidMethod: Self { .init(.invalidMethod)}
/// Failed to extract a path from HTTPRequest
public static var missingPath: Self { .init(.missingPath)}
/// HTTPResponseHead had an invalid status code
public static var invalidStatusCode: Self { .init(.invalidStatusCode)}
}
extension HTTPMethod {
public init(_ newMethod: HTTPRequest.Method) {
switch newMethod {
case .get: self = .GET
case .head: self = .HEAD
case .post: self = .POST
case .put: self = .PUT
case .delete: self = .DELETE
case .connect: self = .CONNECT
case .options: self = .OPTIONS
case .trace: self = .TRACE
case .patch: self = .PATCH
default:
let rawValue = newMethod.rawValue
switch rawValue {
case "ACL": self = .ACL
case "COPY": self = .COPY
case "LOCK": self = .LOCK
case "MOVE": self = .MOVE
case "BIND": self = .BIND
case "LINK": self = .LINK
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 "UNLINK": self = .UNLINK
case "MSEARCH": self = .MSEARCH
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: rawValue)
}
}
}
}
extension HTTPRequest.Method {
public init(_ oldMethod: HTTPMethod) throws {
switch oldMethod {
case .GET: self = .get
case .PUT: self = .put
case .ACL: self = .init("ACL")!
case .HEAD: self = .head
case .POST: self = .post
case .COPY: self = .init("COPY")!
case .LOCK: self = .init("LOCK")!
case .MOVE: self = .init("MOVE")!
case .BIND: self = .init("BIND")!
case .LINK: self = .init("LINK")!
case .PATCH: self = .patch
case .TRACE: self = .trace
case .MKCOL: self = .init("MKCOL")!
case .MERGE: self = .init("MERGE")!
case .PURGE: self = .init("PURGE")!
case .NOTIFY: self = .init("NOTIFY")!
case .SEARCH: self = .init("SEARCH")!
case .UNLOCK: self = .init("UNLOCK")!
case .REBIND: self = .init("REBIND")!
case .UNBIND: self = .init("UNBIND")!
case .REPORT: self = .init("REPORT")!
case .DELETE: self = .delete
case .UNLINK: self = .init("UNLINK")!
case .CONNECT: self = .connect
case .MSEARCH: self = .init("MSEARCH")!
case .OPTIONS: self = .options
case .PROPFIND: self = .init("PROPFIND")!
case .CHECKOUT: self = .init("CHECKOUT")!
case .PROPPATCH: self = .init("PROPPATCH")!
case .SUBSCRIBE: self = .init("SUBSCRIBE")!
case .MKCALENDAR: self = .init("MKCALENDAR")!
case .MKACTIVITY: self = .init("MKACTIVITY")!
case .UNSUBSCRIBE: self = .init("UNSUBSCRIBE")!
case .SOURCE: self = .init("SOURCE")!
case .RAW(value: let value):
guard let method = HTTPRequest.Method(value) else {
throw HTTP1TypeConversionError.invalidMethod
}
self = method
}
}
}
extension HTTPHeaders {
public init(_ newFields: HTTPFields) {
let fields = newFields.map { ($0.name.rawName, $0.value) }
self.init(fields)
}
}
extension HTTPFields {
public init(_ oldHeaders: HTTPHeaders, splitCookie: Bool) {
self.init()
self.reserveCapacity(count)
var firstHost = true
for field in oldHeaders {
if firstHost && field.name.lowercased() == "host" {
firstHost = false
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, *) {
self.append(contentsOf: field.value.split(separator: "; ", omittingEmptySubsequences: false).map {
HTTPField(name: name, value: String($0))
})
} else {
self.append(HTTPField(name: name, value: field.value))
}
}
}
}
}
extension HTTPRequestHead {
public init(_ newRequest: HTTPRequest) throws {
guard let path = newRequest.method == .connect ? newRequest.authority : newRequest.path else {
throw HTTP1TypeConversionError.missingPath
}
var headers = HTTPHeaders()
headers.reserveCapacity(newRequest.headerFields.count + 1)
if let authority = newRequest.authority {
headers.add(name: "Host", value: authority)
}
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: path,
headers: headers
)
}
}
extension HTTPRequest {
public init(_ oldRequest: HTTPRequestHead, secure: Bool, splitCookie: Bool) throws {
let method = try Method(oldRequest.method)
let scheme = secure ? "https" : "http"
let authority = oldRequest.headers["Host"].first
self.init(
method: method,
scheme: scheme,
authority: authority,
path: oldRequest.uri,
headerFields: HTTPFields(oldRequest.headers, splitCookie: splitCookie)
)
}
}
extension HTTPResponseHead {
public init(_ newResponse: HTTPResponse) {
self.init(
version: .http1_1,
status: HTTPResponseStatus(
statusCode: newResponse.status.code,
reasonPhrase: newResponse.status.reasonPhrase
),
headers: HTTPHeaders(newResponse.headerFields)
)
}
}
extension HTTPResponse {
public init(_ oldResponse: HTTPResponseHead) throws {
guard oldResponse.status.code <= 999 else {
throw HTTP1TypeConversionError.invalidStatusCode
}
let status = HTTPResponse.Status(code: Int(oldResponse.status.code), reasonPhrase: oldResponse.status.reasonPhrase)
self.init(status: status, headerFields: HTTPFields(oldResponse.headers, splitCookie: false))
}
}

View File

@ -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 NIOHPACK
import NIOHTTP2
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 {
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 {
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
}
}

View File

@ -0,0 +1,247 @@
//===----------------------------------------------------------------------===//
//
// 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 HTTPTypes
import NIOCore
import NIOHPACK
import NIOHTTP2
import NIOHTTPTypes
// MARK: - Client
private struct BaseClientCodec {
private var headerStateMachine: HTTP2HeadersStateMachine = .init(mode: .client)
private var outgoingHTTP1RequestHead: HTTPRequest?
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPResponsePart?, second: HTTPResponsePart?) {
switch data {
case .headers(let headerContent):
switch try self.headerStateMachine.newHeaders(block: headerContent.headers) {
case .trailer:
let newTrailers = try HTTPFields(trailers: headerContent.headers)
return (first: .end(newTrailers), second: nil)
case .informationalResponseHead:
let newResponse = try HTTPResponse(headerContent.headers)
return (first: .head(newResponse), second: nil)
case .finalResponseHead:
guard self.outgoingHTTP1RequestHead != nil else {
preconditionFailure("Expected not to get a response without having sent a request")
}
self.outgoingHTTP1RequestHead = nil
let newResponse = try HTTPResponse(headerContent.headers)
let first = HTTPResponsePart.head(newResponse)
var second: HTTPResponsePart?
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 = HTTPResponsePart.body(b)
var second: HTTPResponsePart?
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: HTTPRequestPart, 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: .byteBuffer(body)))
case .end(let trailers):
if let 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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPResponsePart
public typealias OutboundIn = HTTPRequestPart
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 {
context.fireChannelRead(self.wrapInboundOut(first))
}
if let 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 = .init(mode: .server)
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPRequestPart?, second: HTTPRequestPart?) {
switch data {
case .headers(let headerContent):
if case .trailer = try self.headerStateMachine.newHeaders(block: headerContent.headers) {
let newTrailers = try HTTPFields(trailers: headerContent.headers)
return (first: .end(newTrailers), second: nil)
} else {
let newRequest = try HTTPRequest(headerContent.headers)
let first = HTTPRequestPart.head(newRequest)
var second: HTTPRequestPart?
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 = HTTPRequestPart.body(b)
var second: HTTPRequestPart?
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: HTTPResponsePart, 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: .byteBuffer(body))
return .data(payload)
case .end(let trailers):
if let 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: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPRequestPart
public typealias OutboundIn = HTTPResponsePart
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 {
context.fireChannelRead(self.wrapInboundOut(first))
}
if let 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)
}
}

View File

@ -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 HTTPTypes
import NIOHPACK
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
}
private extension HPACKIndexing {
init(_ newIndexingStrategy: HTTPField.DynamicTableIndexingStrategy) {
switch newIndexingStrategy {
case .avoid: self = .nonIndexable
case .disallow: self = .neverIndexed
default: self = .indexable
}
}
}
private extension HTTPField.DynamicTableIndexingStrategy {
init(_ oldIndexing: HPACKIndexing) {
switch oldIndexing {
case .indexable: self = .automatic
case .nonIndexable: self = .avoid
case .neverIndexed: self = .disallow
}
}
}
extension HPACKHeaders {
private mutating func add(newField field: HTTPField) {
self.add(name: field.name.canonicalName, value: field.value, indexing: HPACKIndexing(field.indexingStrategy))
}
init(_ newRequest: HTTPRequest) {
self.init()
self.reserveCapacity(newRequest.headerFields.count + 5)
self.add(newField: newRequest.pseudoHeaderFields.method)
if let field = newRequest.pseudoHeaderFields.scheme {
self.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.authority {
self.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.path {
self.add(newField: field)
}
if let field = newRequest.pseudoHeaderFields.extendedConnectProtocol {
self.add(newField: field)
}
for field in newRequest.headerFields {
self.add(newField: field)
}
}
init(_ newResponse: HTTPResponse) {
self.init()
self.reserveCapacity(newResponse.headerFields.count + 1)
self.add(newField: newResponse.pseudoHeaderFields.status)
for field in newResponse.headerFields {
self.add(newField: field)
}
}
init(_ newTrailers: HTTPFields) {
self.init()
self.reserveCapacity(newTrailers.count)
for field in newTrailers {
self.add(newField: field)
}
}
}
extension HTTPRequest {
init(_ hpack: HPACKHeaders) 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 = hpack.startIndex
while i != hpack.endIndex {
let (name, value, indexable) = hpack[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 = hpack.index(after: i)
}
guard let methodString else {
throw HTTP2TypeConversionError.missingMethod
}
guard let method = HTTPRequest.Method(methodString) else {
throw HTTP2TypeConversionError.invalidMethod
}
self.init(
method: method,
scheme: schemeString,
authority: authorityString,
path: pathString
)
self.pseudoHeaderFields.method.indexingStrategy = .init(methodIndexable)
self.pseudoHeaderFields.scheme?.indexingStrategy = .init(schemeIndexable)
self.pseudoHeaderFields.authority?.indexingStrategy = .init(authorityIndexable)
self.pseudoHeaderFields.path?.indexingStrategy = .init(pathIndexable)
if let protocolString {
self.extendedConnectProtocol = protocolString
self.pseudoHeaderFields.extendedConnectProtocol?.indexingStrategy = .init(protocolIndexable)
}
self.headerFields.reserveCapacity(hpack.count)
while i != hpack.endIndex {
let (name, value, indexable) = hpack[i]
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldNotFirst
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = .init(indexable)
self.headerFields.append(field)
}
i = hpack.index(after: i)
}
}
}
extension HTTPResponse {
init(_ hpack: HPACKHeaders) throws {
var statusString: String? = nil
var statusIndexable: HPACKIndexing = .indexable
var i = hpack.startIndex
while i != hpack.endIndex {
let (name, value, indexable) = hpack[i]
if !name.hasPrefix(":") {
break
}
switch name {
case ":status":
if statusString != nil {
throw HTTP2TypeConversionError.multipleStatus
}
statusString = value
statusIndexable = indexable
default:
continue
}
i = hpack.index(after: i)
}
guard let statusString else {
throw HTTP2TypeConversionError.missingStatus
}
guard let status = Int(statusString),
(0 ... 999).contains(status) else {
throw HTTP2TypeConversionError.invalidStatus
}
self.init(status: HTTPResponse.Status(code: status))
self.pseudoHeaderFields.status.indexingStrategy = .init(statusIndexable)
self.headerFields.reserveCapacity(hpack.count)
while i != hpack.endIndex {
let (name, value, indexable) = hpack[i]
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldNotFirst
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = .init(indexable)
self.headerFields.append(field)
}
i = hpack.index(after: i)
}
}
}
extension HTTPFields {
init(trailers: HPACKHeaders) throws {
self.init()
self.reserveCapacity(trailers.count)
for (name, value, indexable) in trailers {
if name.hasPrefix(":") {
throw HTTP2TypeConversionError.pseudoFieldInTrailers
}
if let fieldName = HTTPField.Name(name) {
var field = HTTPField(name: fieldName, value: value)
field.indexingStrategy = .init(indexable)
self.append(field)
}
}
}
}

View File

@ -14,6 +14,8 @@
#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#else
import Glibc
#endif

View File

@ -97,7 +97,7 @@ class DebugInboundEventsHandlerTest: XCTestCase {
}
extension DebugInboundEventsHandler.Event: Equatable {
extension DebugInboundEventsHandler.Event {
public static func == (lhs: DebugInboundEventsHandler.Event, rhs: DebugInboundEventsHandler.Event) -> Bool {
switch (lhs, rhs) {
case (.registered, .registered):
@ -123,3 +123,14 @@ extension DebugInboundEventsHandler.Event: Equatable {
}
}
}
#if swift(>=5.8)
#if $RetroactiveAttribute
extension DebugInboundEventsHandler.Event: @retroactive Equatable { }
#else
extension DebugInboundEventsHandler.Event: Equatable { }
#endif
#else
extension DebugInboundEventsHandler.Event: Equatable { }
#endif

View File

@ -85,7 +85,7 @@ class DebugOutboundEventsHandlerTest: XCTestCase {
}
extension DebugOutboundEventsHandler.Event: Equatable {
extension DebugOutboundEventsHandler.Event {
public static func == (lhs: DebugOutboundEventsHandler.Event, rhs: DebugOutboundEventsHandler.Event) -> Bool {
switch (lhs, rhs) {
case (.register, .register):
@ -108,6 +108,15 @@ extension DebugOutboundEventsHandler.Event: Equatable {
return false
}
}
}
#if swift(>=5.8)
#if $RetroactiveAttribute
extension DebugOutboundEventsHandler.Event: @retroactive Equatable { }
#else
extension DebugOutboundEventsHandler.Event: Equatable { }
#endif
#else
extension DebugOutboundEventsHandler.Event: Equatable { }
#endif

View File

@ -214,4 +214,59 @@ class LineBasedFrameDecoderTest: XCTestCase {
XCTFail("Unexpected error: \(error)")
}
}
func testBasicSingleStep() {
let decoder = LineBasedFrameDecoder()
let b2mp = NIOSingleStepByteToMessageProcessor(decoder)
var callCount = 0
XCTAssertNoThrow(try b2mp.process(buffer: ByteBuffer(string: "1\n\n2\n3\n")) { line in
callCount += 1
switch callCount {
case 1:
XCTAssertEqual(ByteBuffer(string: "1"), line)
case 2:
XCTAssertEqual(ByteBuffer(string: ""), line)
case 3:
XCTAssertEqual(ByteBuffer(string: "2"), line)
case 4:
XCTAssertEqual(ByteBuffer(string: "3"), line)
default:
XCTFail("not expecting call no \(callCount)")
}
})
}
func testBasicSingleStepNoNewlineComingButEOF() {
let decoder = LineBasedFrameDecoder()
let b2mp = NIOSingleStepByteToMessageProcessor(decoder)
XCTAssertNoThrow(try b2mp.process(buffer: ByteBuffer(string: "new newline eva\r")) { line in
XCTFail("not taking calls")
})
XCTAssertThrowsError(try b2mp.finishProcessing(seenEOF: true, { line in
XCTFail("not taking calls")
})) { error in
if let error = error as? NIOExtrasErrors.LeftOverBytesError {
XCTAssertEqual(ByteBuffer(string: "new newline eva\r"), error.leftOverBytes)
} else {
XCTFail("unexpected error: \(error)")
}
}
}
func testBasicSingleStepNoNewlineOrEOFComing() {
let decoder = LineBasedFrameDecoder()
let b2mp = NIOSingleStepByteToMessageProcessor(decoder)
XCTAssertNoThrow(try b2mp.process(buffer: ByteBuffer(string: "new newline eva\r")) { line in
XCTFail("not taking calls")
})
XCTAssertThrowsError(try b2mp.finishProcessing(seenEOF: false, { line in
XCTFail("not taking calls")
})) { error in
if let error = error as? NIOExtrasErrors.LeftOverBytesError {
XCTAssertEqual(ByteBuffer(string: "new newline eva\r"), error.leftOverBytes)
} else {
XCTFail("unexpected error: \(error)")
}
}
}
}

View File

@ -66,7 +66,7 @@ class PCAPRingBufferTest: XCTestCase {
func testByteLimit() {
let expectedData = 150 + 25 + 75 + 120
let ringBuffer = NIOPCAPRingBuffer(maximumFragments: 1000, maximumBytes: expectedData + 10)
let ringBuffer = NIOPCAPRingBuffer(maximumBytes: expectedData + 10)
for fragment in dataForTests() {
ringBuffer.addFragment(fragment)
}

View File

@ -65,6 +65,7 @@ fileprivate func withTemporaryFile<T>(content: String? = nil, _ body: (NIOCore.N
return try body(fileHandle, temporaryFilePath)
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
fileprivate func withTemporaryFile<T>(content: String? = nil, _ body: (NIOCore.NIOFileHandle, String) async throws -> T) async throws -> T {
let temporaryFilePath = "\(temporaryDirectory)/nio_extras_\(UUID())"
FileManager.default.createFile(atPath: temporaryFilePath, contents: content?.data(using: .utf8))

View File

@ -143,6 +143,21 @@ class HTTPResponseDecompressorTest: XCTestCase {
}
}
func testDecompressionMultipleWriteWithLimit() {
let channel = EmbeddedChannel()
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .size(272))).wait())
let headers = HTTPHeaders([("Content-Encoding", "deflate")])
// this compressed payload is 272 bytes long uncompressed
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
for i in 0..<3 {
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))), "\(i)")
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)), "\(i)")
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.end(nil)), "\(i)")
}
}
func testDecompression() {
let channel = EmbeddedChannel()
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait())

View File

@ -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 HTTPTypes
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import XCTest
/// 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))
}
}
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<HTTPResponsePart>()
try self.channel.pipeline.addHandlers(HTTP1ToHTTPClientCodec(), recorder).wait()
try self.channel.writeOutbound(HTTPRequestPart.head(Self.request))
try self.channel.writeOutbound(HTTPRequestPart.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 self.channel.finish().isClean)
}
func testServerHTTP1ToHTTP() throws {
let recorder = InboundRecorder<HTTPRequestPart>()
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(HTTPResponsePart.head(Self.response))
try self.channel.writeOutbound(HTTPResponsePart.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 self.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: HTTPRequestPart.self), .head(Self.request))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPRequestPart.self), .end(Self.trailers))
try self.channel.writeInbound(HTTPResponsePart.head(Self.response))
try self.channel.writeInbound(HTTPResponsePart.end(Self.trailers))
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.oldResponse))
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.oldTrailers))
XCTAssertTrue(try self.channel.finish().isClean)
}
func testServerHTTPToHTTP1() throws {
let recorder = InboundRecorder<HTTPServerRequestPart>()
try self.channel.pipeline.addHandlers(HTTPToHTTP1ServerCodec(), recorder).wait()
try self.channel.writeInbound(HTTPRequestPart.head(Self.request))
try self.channel.writeInbound(HTTPRequestPart.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: HTTPResponsePart.self), .head(Self.response))
XCTAssertEqual(try self.channel.readOutbound(as: HTTPResponsePart.self), .end(Self.trailers))
XCTAssertTrue(try self.channel.finish().isClean)
}
}

View File

@ -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 HTTPTypes
import NIOCore
import NIOEmbedded
import NIOHPACK
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP2
import XCTest
/// 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))
}
}
extension HTTPField.Name {
static let xFoo = Self("X-Foo")!
}
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<HTTPResponsePart>()
try self.channel.pipeline.addHandlers(HTTP2FramePayloadToHTTPClientCodec(), recorder).wait()
try self.channel.writeOutbound(HTTPRequestPart.head(Self.request))
try self.channel.writeOutbound(HTTPRequestPart.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 self.channel.finish().isClean)
}
func testServerHTTP2ToHTTP() throws {
let recorder = InboundRecorder<HTTPRequestPart>()
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(HTTPResponsePart.head(Self.response))
try self.channel.writeOutbound(HTTPResponsePart.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 self.channel.finish().isClean)
}
}

View File

@ -6,7 +6,8 @@ services:
image: swift-nio-extras:22.04-5.10
build:
args:
base_image: "swiftlang/swift:nightly-5.10-jammy"
ubuntu_version: "jammy"
swift_version: "5.10"
test:
image: swift-nio-extras:22.04-5.10

View File

@ -1,19 +0,0 @@
version: "3"
services:
runtime-setup:
image: swift-nio-extras:22.04-5.7
build:
args:
ubuntu_version: "jammy"
swift_version: "5.7"
documentation-check:
image: swift-nio-extras:22.04-5.7
test:
image: swift-nio-extras:22.04-5.7
shell:
image: swift-nio-extras:22.04-5.7