mirror of
https://github.com/apple/swift-nio-extras.git
synced 2025-05-14 17:02:43 +08:00
Merge branch 'main' into dn-automatic-compression-format-detection
This commit is contained in:
commit
43bf9ef378
2
.spi.yml
2
.spi.yml
@ -1,4 +1,4 @@
|
||||
version: 1
|
||||
builder:
|
||||
configs:
|
||||
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS]
|
||||
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2]
|
||||
|
@ -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
|
||||
)
|
||||
|
13
README.md
13
README.md
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -13,6 +13,8 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#else
|
||||
import Glibc
|
||||
#endif
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#else
|
||||
import Glibc
|
||||
#endif
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#else
|
||||
import Glibc
|
||||
#endif
|
||||
|
@ -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 {
|
||||
|
42
Sources/NIOHTTPTypes/NIOHTTPTypes.swift
Normal file
42
Sources/NIOHTTPTypes/NIOHTTPTypes.swift
Normal 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?)
|
||||
}
|
117
Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift
Normal file
117
Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
134
Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift
Normal file
134
Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
225
Sources/NIOHTTPTypesHTTP1/HTTPTypeConversion.swift
Normal file
225
Sources/NIOHTTPTypesHTTP1/HTTPTypeConversion.swift
Normal 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))
|
||||
}
|
||||
}
|
118
Sources/NIOHTTPTypesHTTP2/HTTP2HeadersStateMachine.swift
Normal file
118
Sources/NIOHTTPTypesHTTP2/HTTP2HeadersStateMachine.swift
Normal 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
|
||||
}
|
||||
}
|
247
Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift
Normal file
247
Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift
Normal 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)
|
||||
}
|
||||
}
|
263
Sources/NIOHTTPTypesHTTP2/HTTPTypeConversion.swift
Normal file
263
Sources/NIOHTTPTypesHTTP2/HTTPTypeConversion.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,8 @@
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#else
|
||||
import Glibc
|
||||
#endif
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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())
|
||||
|
170
Tests/NIOHTTPTypesHTTP1Tests/NIOHTTPTypesHTTP1Tests.swift
Normal file
170
Tests/NIOHTTPTypesHTTP1Tests/NIOHTTPTypesHTTP1Tests.swift
Normal 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)
|
||||
}
|
||||
}
|
142
Tests/NIOHTTPTypesHTTP2Tests/NIOHTTPTypesHTTP2Tests.swift
Normal file
142
Tests/NIOHTTPTypesHTTP2Tests/NIOHTTPTypesHTTP2Tests.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user